Skip to content

feat(orchestrator-form): add field-level validation via ui:validateOn and ui:validateGroup - #4156

Open
lokanandaprabhu wants to merge 7 commits into
redhat-developer:mainfrom
lokanandaprabhu:feat/orchestrator-field-level-validation
Open

feat(orchestrator-form): add field-level validation via ui:validateOn and ui:validateGroup#4156
lokanandaprabhu wants to merge 7 commits into
redhat-developer:mainfrom
lokanandaprabhu:feat/orchestrator-field-level-validation

Conversation

@lokanandaprabhu

@lokanandaprabhu lokanandaprabhu commented Aug 4, 2026

Copy link
Copy Markdown
Member

Story: https://redhat.atlassian.net/browse/RHIDP-15871

Summary

  • Add ui:validateOn schema annotation to trigger per-field async validation on blur, change, or blur,change without waiting for Next/Submit
  • Add ui:validateGroup schema annotation for dependent field group validation — when all group members have values, validation triggers for all members automatically
  • Wire onBlur in ActiveTextInput, ActiveDropdown, and ActiveMultiSelect widgets
  • Fully backward compatible — fields without annotations keep existing Next-button-only validation

New schema annotations

Annotation Values Description
ui:validateOn "blur", "change", "blur,change" When to trigger field-level validation
ui:validateGroup Any string (group name) Links fields for group validation

Packages affected

  • orchestrator-form-api — added validatingFields and getExtraErrorsForField types
  • orchestrator-form-react — validation orchestration (useFieldValidation, fieldValidationConfig, mergeExtraErrors)
  • orchestrator-form-widgets — single-field validation (validateSingleField, useGetExtraErrorsForField), widget onBlur wiring

Video:

Screen.Recording.2026-08-04.at.1.24.07.PM.mov

How to test

Add the following proxy to your app-config.local.yaml:

proxy:
  endpoints:
    "/postman-echo":
      target: "https://postman-echo.com"
      changeOrigin: true
      allowedMethods: ["GET", "POST"]

Create the test workflow file at packages/backend/.devModeTemp/repository/workflows/test-field-validation.sw.yaml:

id: test-field-validation
version: '1.0'
specVersion: '0.8'
name: Test Field Validation
description: Test workflow for ui:validateOn and ui:validateGroup annotations
dataInputSchema: schemas/test-field-validation__main-schema.json
start: LogState
functions:
  - name: logFunction
    type: custom
    operation: sysout
states:
  - name: LogState
    type: operation
    actions:
      - name: logMessage
        functionRef:
          refName: logFunction
          arguments:
            message: 'Workflow started with input: ${.}'
    end: true

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

  1. Blur validation — Type in "Blur-validated field", tab out → network call fires immediately
  2. Change validation — Type in "Change-validated field" → network call fires after 1s debounce
  3. Blur+Change — Try both interactions on "Blur+Change validated field"
  4. No annotation (backward compat) — Type and blur "No field-level validation" → no network call until Next
  5. Group validation — Fill only Namespace, blur → validates only Namespace. Fill both Namespace + Cluster, blur either → validates both
  6. Error display — Change a validate:url to postman-echo/status/400 → error appears on blur/change

Test plan

  • Unit tests for fieldValidationConfig (parseValidateOn, getFieldValidationConfig, getGroupMembers, areAllGroupFieldsPopulated)
  • Unit tests for mergeExtraErrors (merge, replace, clear, preserve, immutability)
  • Unit tests for validateSingleField (validate:url handling, error responses, widget type filtering)
  • Unit tests for FormWidgetsApi (getExtraErrorsForField prop passing)
  • All existing tests pass (17 suites/171 tests in form-react, 18 suites/85 tests in form-widgets)
  • npx tsc --noEmit passes clean
  • All three packages build successfully
  • Manual test with test workflow above

… 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>
@rhdh-gh-app

rhdh-gh-app Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-orchestrator-form-api workspaces/orchestrator/plugins/orchestrator-form-api minor v2.9.0
@red-hat-developer-hub/backstage-plugin-orchestrator-form-react workspaces/orchestrator/plugins/orchestrator-form-react minor v2.10.0
@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets workspaces/orchestrator/plugins/orchestrator-form-widgets minor v1.12.0

@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

Add field-level async validation via ui:validateOn and ui:validateGroup

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add ui:validateOn/ui:validateGroup to trigger async validation on blur/change.
• Orchestrate debounced per-field validation, group revalidation, and extraErrors merging.
• Document new annotations and add unit tests for config/error/validator utilities.
Diagram

graph TD
  U(["User"]) --> W(["Active widgets"]) --> F["OrchestratorFormWrapper"] --> H["useFieldValidation"] --> V["validateSingleField"] --> X{{"validate:url endpoint"}}
  H --> C["fieldValidationConfig"] --> H
  H --> E["extraErrors state"]
  subgraph Legend
    direction LR
    _ui(["UI component"]) ~~~ _logic["Logic / hook"] ~~~ _ext{{"External endpoint"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use RJSF liveValidate/customValidate pipeline for async field validation
  • ➕ Keeps validation inside RJSF’s standard validation lifecycle
  • ➕ Potentially reduces custom state management around extraErrors
  • ➖ RJSF validation hooks are primarily synchronous; async field-level validation often requires custom plumbing anyway
  • ➖ Harder to implement debounce + per-field request cancellation/ordering reliably inside core validation hooks
2. Implement field-level validation purely at widget layer (no wrapper hook)
  • ➕ Keeps event handling co-located with inputs and avoids global orchestration
  • ➕ Could simplify mapping RJSF ids to dotted paths in the wrapper
  • ➖ Requires duplicating debounce/request-ordering/group logic across multiple widgets
  • ➖ Harder to manage shared concerns like validateGroup fan-out and centralized extraErrors merging

Recommendation: Current approach (central orchestration in orchestrator-form-react via useFieldValidation + a reusable getExtraErrorsForField/validateSingleField in widgets) is the best tradeoff: it keeps widget wiring minimal (just passing onBlur), consolidates debounce and request-ordering in one place, and preserves backward-compatible Next/Submit validation via existing getExtraErrors.

Files changed (18) +1087 / -25

Enhancement (12) +519 / -23
api.tsExpose validatingFields and getExtraErrorsForField in API types +6/-0

Expose validatingFields and getExtraErrorsForField in API types

• Extends 'OrchestratorFormContextProps' with an optional 'validatingFields' set to indicate in-flight field validations. Adds a 'getExtraErrorsForField' callback type to 'FormDecoratorProps' for single-field async validation.

workspaces/orchestrator/plugins/orchestrator-form-api/src/api.ts

OrchestratorFormWrapper.tsxWire onBlur/onChange triggers into new field-validation hook +47/-19

Wire onBlur/onChange triggers into new field-validation hook

• Adds 'useFieldValidation' to trigger async field validation on blur/change based on uiSchema annotations, and injects 'validatingFields' into the formContext. Plumbs RJSF 'onBlur' into the form and ensures per-field errors are merged into 'extraErrors' without clearing other fields’ errors.

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorFormWrapper.tsx

fieldValidationConfig.tsAdd helpers for ui:validateOn and ui:validateGroup config +95/-0

Add helpers for ui:validateOn and ui:validateGroup config

• Introduces utilities to parse 'ui:validateOn' into allowed modes, read field-level validation config from uiSchema, enumerate validateGroup members (including nested schemas), and determine when a group is fully populated.

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/fieldValidationConfig.ts

mergeExtraErrors.tsAdd mergeExtraErrors helper for targeted per-field updates +41/-0

Add mergeExtraErrors helper for targeted per-field updates

• Implements a utility that removes existing errors for a specific field path and sets new field errors returned by async validation. Returns 'undefined' when the resulting error schema is empty.

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/mergeExtraErrors.ts

useFieldValidation.tsAdd debounced per-field validation hook with group fan-out +150/-0

Add debounced per-field validation hook with group fan-out

• Introduces 'useFieldValidation' to orchestrate field-level async validation based on 'ui:validateOn', with a 1s debounce on change and immediate validation on blur. Tracks in-flight validations via 'validatingFields', de-dupes out-of-order responses with request ids, and triggers group-member validation when all members are populated.

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts

FormDecoratorContent.tsxProvide getExtraErrorsForField to the form wrapper +3/-1

Provide getExtraErrorsForField to the form wrapper

• Extends the widget decorator to supply both step-level 'getExtraErrors' and new single-field 'getExtraErrorsForField'. Keeps existing behavior intact while enabling incremental validation for annotated fields.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx

index.tsExport single-field validation utilities +2/-0

Export single-field validation utilities

• Exports 'validateSingleField' and 'useGetExtraErrorsForField' from the widgets utils barrel to make single-field validation available to consumers within the package.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/index.ts

useGetExtraErrorsForField.tsAdd hook to run async validation for one field +42/-0

Add hook to run async validation for one field

• Introduces 'useGetExtraErrorsForField', which binds fetch + template evaluation dependencies and returns an async function producing an 'ErrorSchema' for a specific field path. Delegates the HTTP/templating logic to 'validateSingleField'.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useGetExtraErrorsForField.ts

validateSingleField.tsExtract reusable single-field async validation runner +118/-0

Extract reusable single-field async validation runner

• Implements single-field async validation by evaluating 'validate:url', building request init, calling the validate endpoint, and translating non-200 responses into an RJSF 'ErrorSchema' at the field path. Restricts execution to supported widgets and skips when the field is unset.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/validateSingleField.ts

ActiveDropdown.tsxTrigger form-level onBlur from ActiveDropdown +2/-1

Trigger form-level onBlur from ActiveDropdown

• Plumbs RJSF’s 'onBlur' into the ActiveDropdown widget and invokes it with the field id and current value. Enables wrapper-level blur-triggered validation when 'ui:validateOn' is configured.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx

ActiveMultiSelect.tsxTrigger form-level onBlur from ActiveMultiSelect +10/-1

Trigger form-level onBlur from ActiveMultiSelect

• Adds 'onBlur' handling to ActiveMultiSelect and wires it to the underlying input, passing id and selected values. This allows the orchestrator form wrapper to initiate blur-triggered field validation.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx

ActiveTextInput.tsxTrigger form-level onBlur from ActiveTextInput +3/-1

Trigger form-level onBlur from ActiveTextInput

• Adds 'onBlur' handling to both autocomplete and standard text input render paths and calls it with the id and current value. Enables blur-triggered validation for ActiveTextInput fields annotated with 'ui:validateOn'.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx

Tests (4) +434 / -0
fieldValidationConfig.test.tsUnit tests for validateOn parsing and validateGroup behavior +193/-0

Unit tests for validateOn parsing and validateGroup behavior

• Adds coverage for parsing 'ui:validateOn', extracting per-field config from nested uiSchema paths, discovering group members, and checking whether group fields are populated. Verifies invalid input handling and nested path support.

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/fieldValidationConfig.test.ts

mergeExtraErrors.test.tsUnit tests for per-field extraErrors merging +81/-0

Unit tests for per-field extraErrors merging

• Adds tests validating that per-field async errors replace only the targeted field path, preserve unrelated fields, and clear state when a field becomes valid. Ensures the merge helper does not mutate existing error schemas.

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/mergeExtraErrors.test.ts

FormWidgetsApi.test.tsxUpdate FormWidgetsApi tests for getExtraErrorsForField +8/-0

Update FormWidgetsApi tests for getExtraErrorsForField

• Updates the widgets API tests/mocks to include 'useGetExtraErrorsForField' and asserts the resulting prop is passed to the decorated form component. Ensures the new hook is wired without breaking existing props.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.test.tsx

validateSingleField.test.tsUnit tests for validateSingleField HTTP validation behavior +152/-0

Unit tests for validateSingleField HTTP validation behavior

• Adds coverage for early-exit cases (no validate:url, unsupported widget, undefined value) and for success/failure responses from the validation endpoint. Verifies error messages when validate:url templating does not produce a string.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/validateSingleField.test.ts

Documentation (1) +127 / -2
orchestratorFormWidgets.mdDocument ui:validateOn/ui:validateGroup field-level validation +127/-2

Document ui:validateOn/ui:validateGroup field-level validation

• Adds a new documentation section describing field-level validation triggers on blur/change (including debounce behavior) and dependent group validation. Updates validate:url docs to reference the new annotations and provides JSON examples.

workspaces/orchestrator/docs/orchestratorFormWidgets.md

Other (1) +7 / -0
field-level-validation.mdAdd changeset for field-level validation minors +7/-0

Add changeset for field-level validation minors

• Introduces a changeset bumping minor versions for the form api/react/widgets packages. Documents the new 'ui:validateOn' and 'ui:validateGroup' capabilities at release-note level.

workspaces/orchestrator/.changeset/field-level-validation.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Unhandled async validation errors ✓ Resolved 🐞 Bug ☼ Reliability
Description
useFieldValidation.validateField does not catch exceptions from getExtraErrorsForField (which
can throw on network failures), and callers invoke validateField(...) without awaiting it.
This can produce unhandled rejected promises and leave the field without a deterministic validation
result.
Code

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts[R81-84]

+        let asyncErrors: ErrorSchema<JsonObject> = {};
+        if (getExtraErrorsForField) {
+          asyncErrors = await getExtraErrorsForField(
+            formData,
Relevance

●●● Strong

Unhandled promise rejections in validation are reliability bugs; team has accepted similar
robustness/testing improvements.

PR-#3522

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
validateField awaits getExtraErrorsForField without a catch; getExtraErrorsForField ultimately
calls validateSingleField, which calls fetchApi.fetch and can throw. Because
triggerFieldValidation invokes validateField without awaiting, thrown errors become unhandled
promise rejections.

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts[67-104]
workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts[106-133]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useGetExtraErrorsForField.ts[25-42]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/validateSingleField.ts[83-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Field-level async validation awaits `getExtraErrorsForField(...)` but does not catch errors. Since `triggerFieldValidation` calls `validateField(...)` without awaiting it, a thrown error becomes an unhandled rejected promise.

## Issue Context
- `validateSingleField` performs `fetchApi.fetch(...)` which can throw on network errors.
- The submit/Next path already has explicit try/catch around async validation; field-level validation should be similarly defensive.

## Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts[67-104]
- workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts[111-121]
- workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/validateSingleField.ts[83-94]

## Suggested fix
- Wrap the `await getExtraErrorsForField(...)` in a `try/catch` and ensure the promise is always resolved (no rejection escape).
- Decide on behavior for failures:
 - convert to a field error (e.g., “Validation request failed”) so the user sees feedback, or
 - log/telemetry + treat as no extra errors.
- Ensure `validatingFields` cleanup still runs (keep finally) and consider explicitly `void validateField(...).catch(...)` if you want belt-and-suspenders at the call sites.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. onBlur handler not guarded ✓ Resolved 🐞 Bug ☼ Reliability
Description
Active widgets call onBlur(id, value) without checking that onBlur exists.
If these widgets are rendered in a context that doesn’t supply an onBlur prop, they will throw a
TypeError when the control blurs.
Code

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx[R229-231]

        onChange={event => handleChange(event.target.value, true)}
+        onBlur={() => onBlur(id, value)}
        label={label}
Relevance

●●● Strong

Guarding optional callback props to avoid runtime TypeError is a low-risk defensive fix.

PR-#2096

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Each widget destructures onBlur from props and invokes it directly in the rendered input’s
onBlur handler; there is no optional chaining or default function, so missing onBlur will cause
a runtime exception.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx[56-66]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx[224-276]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx[57-67]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx[300-332]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx[66-83]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx[352-364]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ActiveTextInput`, `ActiveDropdown`, and `ActiveMultiSelect` call `onBlur(id, value)` unconditionally. If `onBlur` is not passed (e.g., standalone widget usage, partial test props, or alternate form integration), the widget will crash on blur.

## Issue Context
Even though the orchestrator wrapper now passes `onBlur` to the RJSF form, these widgets are library components and should be defensive about optional callback props.

## Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx[224-276]
- workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx[300-332]
- workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx[352-364]

## Suggested fix
- Change call sites to `onBlur?.(id, value)`.
- Alternatively, default destructuring: `const { onBlur = () => {}, ... } = props;` (but prefer optional chaining to preserve intent).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Group check uses stale data ✓ Resolved 🐞 Bug ≡ Correctness
Description
Dependent group validation can be skipped because areAllGroupFieldsPopulated() reads
formDataRef.current synchronously inside triggerFieldValidation, while formDataRef is only
updated in a useEffect after setFormData().
This can incorrectly prevent group-member validation from triggering when the last field is
populated in the same interaction.
Code

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorFormWrapper.tsx[R188-190]

+    if (fieldPath) {
+      triggerFieldValidation(fieldPath, 'change');
+    }
Relevance

●● Moderate

Subtle stale-ref race; plausible but behavioral change is nontrivial and no close precedent found.

PR-#2759
PR-#3522

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
triggerFieldValidation checks group population using formDataRef.current immediately, but
formDataRef is only updated from formContext.formData inside an effect, and
formContext.formData itself is controlled via React state (setFormData). This creates a window
where the group-population check can read stale data and skip group validation.

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorFormWrapper.tsx[74-90]
workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorFormWrapper.tsx[177-191]
workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorForm.tsx[122-176]
workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts[106-133]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Dependent-field group validation makes its “all members populated” decision based on `formDataRef.current`, but that ref is updated via an effect after React state updates. In the same `onChange` event where `setFormData(e.formData)` runs, `triggerFieldValidation()` can still see the previous `formDataRef.current`, causing `areAllGroupFieldsPopulated()` to return false and skip group validation.

## Issue Context
- `OrchestratorFormWrapper` maintains `formDataRef` in a `useEffect`.
- `useFieldValidation.triggerFieldValidation()` performs the group-populated check synchronously (before any debounced validation fires).

## Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorFormWrapper.tsx[74-90]
- workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorFormWrapper.tsx[177-191]
- workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/useFieldValidation.ts[106-133]

## Suggested fix
- In `onChange`, set `formDataRef.current = e.formData || {}` before calling `triggerFieldValidation(...)`.
- (Optional) Consider passing the latest `formData` into `triggerFieldValidation` (or into the group check) instead of relying on an effect-updated ref.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 11 rules
✅ Cross-repo context
  Explored: repo: redhat-developer/rhdh (sha: 3875f708)
  Explored: repo: redhat-developer/rhdh-local (sha: 00e76453)
  Not relevant to this PR: redhat-developer/rhdh-chart
  Not relevant to this PR: redhat-developer/rhdh-operator

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

@rhdh-qodo-merge rhdh-qodo-merge Bot added documentation Improvements or additions to documentation enhancement New feature or request Tests labels Aug 4, 2026
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.40314% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.04%. Comparing base (59a7949) to head (edc56dc).
⚠️ Report is 23 commits behind head on main.
✅ All tests successful. No failed tests found.

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              
Flag Coverage Δ *Carryforward flag
adoption-insights 84.55% <ø> (ø) Carriedforward from 7838267
ai-integrations 69.71% <ø> (ø) Carriedforward from 7838267
app-defaults 69.79% <ø> (ø) Carriedforward from 7838267
augment 46.67% <ø> (ø) Carriedforward from 7838267
boost 76.77% <ø> (ø) Carriedforward from 7838267
bulk-import 72.56% <ø> (ø) Carriedforward from 7838267
cost-management 13.55% <ø> (ø) Carriedforward from 7838267
dcm 60.72% <ø> (ø) Carriedforward from 7838267
extensions 56.59% <ø> (ø) Carriedforward from 7838267
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 7838267
global-header 66.50% <ø> (ø) Carriedforward from 7838267
homepage 47.50% <ø> (ø) Carriedforward from 7838267
install-dynamic-plugins 59.95% <ø> (ø) Carriedforward from 7838267
intelligent-assistant 74.61% <ø> (ø) Carriedforward from 7838267
konflux 91.98% <ø> (ø) Carriedforward from 7838267
lightspeed 69.02% <ø> (ø) Carriedforward from 7838267
mcp-integrations 83.40% <ø> (ø) Carriedforward from 7838267
orchestrator 66.56% <53.40%> (-0.30%) ⬇️
quickstart 63.74% <ø> (ø) Carriedforward from 7838267
sandbox 79.56% <ø> (ø) Carriedforward from 7838267
scorecard 85.34% <ø> (ø) Carriedforward from 7838267
theme 88.52% <ø> (ø) Carriedforward from 7838267
translations 5.12% <ø> (ø) Carriedforward from 7838267
x2a 79.20% <ø> (ø) Carriedforward from 7838267

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 59a7949...edc56dc. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

lokanandaprabhu and others added 2 commits August 4, 2026 13:55
- 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 karthikjeeyar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

lokanandaprabhu and others added 2 commits August 5, 2026 12:58
- 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>
@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@lokanandaprabhu

Copy link
Copy Markdown
Member Author

@karthikjeeyar Updated the PR based on review comments, PTAL.

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 enhancement New feature or request Tests workspace/orchestrator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants