Skip to content

feat(orchestrator-form-widgets): add ActiveBoolean widget for dynamic boolean form fields - #4440

Open
lokanandaprabhu wants to merge 2 commits into
redhat-developer:mainfrom
lokanandaprabhu:feat/active-boolean-widget
Open

feat(orchestrator-form-widgets): add ActiveBoolean widget for dynamic boolean form fields#4440
lokanandaprabhu wants to merge 2 commits into
redhat-developer:mainfrom
lokanandaprabhu:feat/active-boolean-widget

Conversation

@lokanandaprabhu

@lokanandaprabhu lokanandaprabhu commented Aug 24, 2026

Copy link
Copy Markdown
Member

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

Summary

Adds a new ActiveBoolean widget that enables boolean (checkbox) fields in Orchestrator workflow forms to dynamically fetch their values from external APIs, following the same ui:props pattern as existing Active widgets (ActiveTextInput, ActiveDropdown, ActiveMultiSelect).

Changes

  • ✅ New ActiveBoolean widget component
  • ✅ Comprehensive unit tests
  • ✅ Documentation in orchestratorFormWidgets.md
  • ✅ Widget registration in FormDecoratorContent.tsx
  • ✅ Changeset for release notes

Features

  • Supports fetch:url to call external APIs
  • Supports fetch:response:value JSONata selector to extract boolean values
  • Supports fetch:response:default as static fallback
  • Coerces string values ("true", "false", "1", "0") and numbers (1, 0) to boolean
  • Supports fetch:retrigger to refetch when other fields change
  • Supports fetch:clearOnRetrigger to clear value before refetching
  • Supports all standard fetch configuration: retry, error handling, etc.
  • Tracks user changes to prevent overwriting manual edits
  • Shows loading spinner while fetching
  • Respects readOnly schema property

Video:

Screen.Recording.2026-08-24.at.2.01.23.PM.mov

Usage Example

{
  "enableFeature": {
    "type": "boolean",
    "title": "Enable Advanced Features",
    "ui:widget": "ActiveBoolean",
    "ui:props": {
      "fetch:url": "https://api.example.com/feature-flags",
      "fetch:response:value": "features.advanced.enabled",
      "fetch:response:default": false,
      "fetch:retrigger": ["current.tenantId"]
    }
  }
}

Testing Instructions

1. Setup Proxy Configuration

Add this to your app-config.local.yaml:

proxy:
  endpoints:
    "/dummyjson":
      target: "https://dummyjson.com"
      changeOrigin: true
      allowedMethods: ["GET"]
      allowedHeaders: ["Content-Type"]
      headers:
        X-Requested-With: "XMLHttpRequest"

2. Create Test Workflow

Create the following files in your dev environment:

File: packages/backend/.devModeTemp/repository/workflows/test-active-boolean.sw.yaml

id: test-active-boolean
version: '1.0'
specVersion: '0.8'
name: Test ActiveBoolean Widget
description: Testing ActiveBoolean widget with various fetch and ui:props configurations
annotations:
  - "workflow-type/infrastructure"
dataInputSchema: schemas/test-active-boolean__main-schema.json
extensions:
  - extensionid: workflow-output-schema
    outputSchema: schemas/workflow-output-schema.json
start: StartState
functions:
  - name: runActionFunction
    type: custom
    operation: sysout
  - name: successResult
    type: expression
    operation: '{ "result": { "message": "ActiveBoolean test completed successfully" } }'
states:
  - name: StartState
    type: operation
    actions:
      - name: runAction
        functionRef:
          refName: runActionFunction
          arguments:
            message: '"Workflow completed with input: " + (. | tostring)'
      - name: setOutput
        functionRef:
          refName: successResult
    end: true

File: packages/backend/.devModeTemp/repository/workflows/schemas/test-active-boolean__main-schema.json

Click to expand schema (169 lines)
{
  "$id": "classpath:/schemas/test-active-boolean__main-schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Test ActiveBoolean Widget",
  "type": "object",
  "description": "Testing ActiveBoolean widget with various fetch and ui:props configurations",
  "properties": {
    "basicTests": {
      "type": "object",
      "title": "Basic ActiveBoolean Tests",
      "description": "Test basic fetch capabilities of ActiveBoolean widget",
      "properties": {
        "basicFeature": {
          "type": "boolean",
          "title": "Basic Feature Flag",
          "description": "Fetches boolean value from API - uses stock > 50 as boolean",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/1",
            "fetch:response:value": "stock > 50"
          }
        },
        "featureWithDefault": {
          "type": "boolean",
          "title": "Feature with Static Default",
          "description": "Has a static default that shows while fetch is loading",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/2",
            "fetch:response:value": "stock > 100",
            "fetch:response:default": true
          }
        },
        "stringCoercion": {
          "type": "boolean",
          "title": "String to Boolean Coercion",
          "description": "API returns string, widget coerces to boolean",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/3",
            "fetch:response:value": "availabilityStatus != 'Low Stock'",
            "fetch:response:default": false
          }
        },
        "numberCoercion": {
          "type": "boolean",
          "title": "Number to Boolean Coercion",
          "description": "API returns number, widget coerces to boolean",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/4",
            "fetch:response:value": "stock",
            "fetch:response:default": false
          }
        }
      }
    },
    "retriggerTests": {
      "type": "object",
      "title": "Retrigger and Dependency Tests",
      "description": "Test fetch:retrigger and dependent boolean fields",
      "properties": {
        "tenantId": {
          "type": "string",
          "title": "Product ID (Simulating Tenant)",
          "description": "Select a product ID to see different boolean values",
          "enum": ["5", "10", "15"],
          "default": "5"
        },
        "dependentFeature": {
          "type": "boolean",
          "title": "Tenant-Specific Feature",
          "description": "This boolean refetches when Product ID changes",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/$${{current.retriggerTests.tenantId}}",
            "fetch:response:value": "stock > 30",
            "fetch:retrigger": ["current.retriggerTests.tenantId"]
          }
        },
        "clearOnRetriggerFeature": {
          "type": "boolean",
          "title": "Feature with Clear on Retrigger",
          "description": "Clears before refetching",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/$${{current.retriggerTests.tenantId}}",
            "fetch:response:value": "price > 500",
            "fetch:retrigger": ["current.retriggerTests.tenantId"],
            "fetch:clearOnRetrigger": true
          }
        }
      }
    },
    "errorHandlingTests": {
      "type": "object",
      "title": "Error Handling Tests",
      "description": "Test error handling and retry behavior",
      "properties": {
        "silentErrorFeature": {
          "type": "boolean",
          "title": "Silent Error Handling",
          "description": "Errors are suppressed, shows default value",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/999999",
            "fetch:response:value": "stock > 0",
            "fetch:response:default": false,
            "fetch:error:silent": true
          }
        },
        "retryFeature": {
          "type": "boolean",
          "title": "Feature with Retry",
          "description": "Retries up to 3 times with exponential backoff",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/6",
            "fetch:response:value": "stock > 0",
            "fetch:retry:maxAttempts": 3,
            "fetch:retry:delay": 1000,
            "fetch:retry:backoff": 2,
            "fetch:retry:statusCodes": [404, 408, 429, 500, 502, 503, 504]
          }
        }
      }
    },
    "advancedTests": {
      "type": "object",
      "title": "Advanced Tests",
      "description": "Advanced scenarios and edge cases",
      "properties": {
        "readOnlyFeature": {
          "type": "boolean",
          "title": "Read-Only Feature Flag",
          "description": "This boolean is fetched but cannot be changed",
          "readOnly": true,
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/7",
            "fetch:response:value": "stock > 0"
          }
        },
        "skipInitialFeature": {
          "type": "boolean",
          "title": "Skip Initial Value",
          "description": "Doesn't apply fetched value initially",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/1",
            "fetch:response:value": "stock > 50",
            "fetch:skipInitialValue": true
          }
        },
        "complexSelectorFeature": {
          "type": "boolean",
          "title": "Complex JSONata Selector",
          "description": "Uses complex JSONata expression",
          "ui:widget": "ActiveBoolean",
          "ui:props": {
            "fetch:url": "$${{backend.baseUrl}}/api/proxy/dummyjson/products/8",
            "fetch:response:value": "(price > 100) and (stock > 10)",
            "fetch:response:default": false
          }
        }
      }
    }
  }
}

Supported ui:props

All standard Active widget props are supported:

  • fetch:url - API endpoint
  • fetch:response:value - JSONata selector for boolean value
  • fetch:response:default - Static fallback value
  • fetch:retrigger - Array of form field paths to watch
  • fetch:clearOnRetrigger - Clear value before refetching
  • fetch:skipInitialValue - Don't apply initial fetch result
  • fetch:retry:maxAttempts - Number of retry attempts
  • fetch:retry:delay - Initial retry delay (ms)
  • fetch:retry:backoff - Backoff multiplier
  • fetch:retry:statusCodes - HTTP status codes to retry
  • fetch:error:silent - Suppress error messages
  • fetch:method, fetch:headers, fetch:body - Request configuration

… boolean form fields

Add ActiveBoolean widget that enables boolean fields in Orchestrator workflow
forms to dynamically fetch values from external APIs using the same ui:props
pattern as existing Active widgets.

Features:
- Supports fetch:url, fetch:response:value, fetch:response:default
- Coerces string and number values to boolean
- Supports fetch:retrigger and fetch:clearOnRetrigger
- Tracks user changes to prevent overwriting manual edits
- Includes comprehensive unit tests and documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@rhdh-gh-app

rhdh-gh-app Bot commented Aug 24, 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-widgets workspaces/orchestrator/plugins/orchestrator-form-widgets minor v2.0.0

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.69697% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.49%. Comparing base (0184539) to head (47f01b6).
⚠️ Report is 32 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4440   +/-   ##
=======================================
  Coverage   61.49%   61.49%           
=======================================
  Files        2534     2535    +1     
  Lines      101630   101696   +66     
  Branches    28453    28474   +21     
=======================================
+ Hits        62496    62542   +46     
- Misses      37301    37321   +20     
  Partials     1833     1833           
Flag Coverage Δ *Carryforward flag
adoption-insights 84.55% <ø> (ø) Carriedforward from 210d513
ai-integrations 67.70% <ø> (ø) Carriedforward from 210d513
app-defaults 48.37% <ø> (ø) Carriedforward from 210d513
augment 46.67% <ø> (ø) Carriedforward from 210d513
boost 79.32% <ø> (ø) Carriedforward from 210d513
bulk-import 72.79% <ø> (ø) Carriedforward from 210d513
cost-management 13.55% <ø> (ø) Carriedforward from 210d513
dcm 67.21% <ø> (ø) Carriedforward from 210d513
e2e-adoption-insights 60.00% <ø> (ø) Carriedforward from 210d513
e2e-extensions 62.13% <ø> (ø) Carriedforward from 210d513
e2e-global-header 49.45% <ø> (ø) Carriedforward from 210d513
e2e-homepage 43.49% <ø> (ø) Carriedforward from 210d513
e2e-intelligent-assistant 46.68% <ø> (ø) Carriedforward from 210d513
e2e-orchestrator 49.51% <ø> (ø) Carriedforward from 210d513
e2e-quickstart 55.21% <ø> (ø) Carriedforward from 210d513
e2e-scorecard 50.21% <ø> (ø) Carriedforward from 210d513
e2e-theme 16.36% <ø> (ø) Carriedforward from 210d513
extensions 56.59% <ø> (ø) Carriedforward from 210d513
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 210d513
global-header 66.50% <ø> (ø) Carriedforward from 210d513
homepage 47.50% <ø> (ø) Carriedforward from 210d513
install-dynamic-plugins 59.95% <ø> (ø) Carriedforward from 210d513
intelligent-assistant 75.42% <ø> (ø) Carriedforward from 210d513
konflux 91.98% <ø> (ø) Carriedforward from 210d513
lightspeed 69.02% <ø> (ø) Carriedforward from 210d513
mcp-integrations 83.40% <ø> (ø) Carriedforward from 210d513
orchestrator 70.91% <69.69%> (-0.02%) ⬇️
quickstart 63.74% <ø> (ø) Carriedforward from 210d513
sandbox 79.56% <ø> (ø) Carriedforward from 210d513
scorecard 87.36% <ø> (ø) Carriedforward from 210d513
theme 88.91% <ø> (ø) Carriedforward from 210d513
translations 5.12% <ø> (ø) Carriedforward from 210d513
x2a 79.20% <ø> (ø) Carriedforward from 210d513

*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 0184539...47f01b6. 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.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant