This is an automated email from the ASF dual-hosted git repository.

mcgilman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 9fa515bdfc4 NIFI-16206 connector toggle to handle "true"/"false" 
strings as booleans (#11546)
9fa515bdfc4 is described below

commit 9fa515bdfc4c10ab2b54ba20a427b51603b1b2bf
Author: Scott Aslan <[email protected]>
AuthorDate: Tue Aug 25 09:57:03 2026 -0400

    NIFI-16206 connector toggle to handle "true"/"false" strings as booleans 
(#11546)
    
    * NIFI-16206 connector toggle to handle "true"/"false" strings as booleans
    
    * address review feedback
    
    * address review feedback
---
 .../connector-property-input.component.spec.ts     |  10 ++
 .../connector-property-input.component.ts          |   8 +-
 .../connector-configuration-step.component.spec.ts | 110 ++++++++++++++++++-
 .../connector-configuration-step.component.ts      |  30 ++++--
 .../connector-wizard/step-dependency.utils.spec.ts |  24 +++++
 .../connector-wizard/step-dependency.utils.ts      |  10 +-
 .../property-group-card.component.spec.ts          | 119 ++++++++++++++++++---
 .../property-group-card.component.ts               |  20 ++--
 .../src/services/value-reference.helper.spec.ts    |  48 +++++++++
 .../shared/src/services/value-reference.helper.ts  |  23 +++-
 .../shared/src/utils/connector-validation.utils.ts |   6 +-
 .../src/utils/dependency-value.utils.spec.ts       |  45 ++++++++
 .../shared/src/utils/dependency-value.utils.ts     |  39 +++++++
 13 files changed, 435 insertions(+), 57 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
index 2b7a75001ee..101e5f53f44 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
@@ -489,6 +489,16 @@ describe('ConnectorPropertyInput', () => {
             const toggle = 
fixture.debugElement.query(By.css('[data-qa="property-input-boolean"]'));
             expect(toggle.componentInstance.checked).toBe(false);
         });
+
+        it('does not coerce values for non-BOOLEAN properties', async () => {
+            const { inputComponent } = await setup({
+                property: makeProp({ type: 'STRING' })
+            });
+
+            inputComponent.writeValue('false');
+
+            expect(inputComponent.formControl.value).toBe('false');
+        });
     });
 
     describe('STRING_LIST rendering', () => {
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
index ba3d5a5f8ff..711b91a7147 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
@@ -47,6 +47,7 @@ import {
 } from '../../types';
 import { SearchableSelect } from 
'../searchable-select/searchable-select.component';
 import { AssetUpload } from '../asset-upload/asset-upload.component';
+import { toBooleanValue } from '../../services/value-reference.helper';
 import { StringListOrphansStrippedEvent } from 
'./connector-property-input.types';
 
 /**
@@ -189,10 +190,9 @@ export class ConnectorPropertyInput implements 
ControlValueAccessor, DoCheck, On
     }
 
     writeValue(value: unknown): void {
-        let normalized = value;
-        if (this.property()?.type === 'BOOLEAN') {
-            normalized = value === true || value === 'true';
-        }
+        // BOOLEAN values may still arrive as the wire strings "true"/"false". 
MatSlideToggle
+        // coerces with `!!value`, which would render "false" as checked, so 
normalize first.
+        const normalized = this.property()?.type === 'BOOLEAN' ? 
toBooleanValue(value) : value;
         this.formControl.setValue(normalized, { emitEvent: false });
         if (this.property()?.type === 'SECRET') {
             this.selectOptions = this.computeSelectOptions();
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
index 1e785529297..8b74f7a83d3 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
@@ -328,6 +328,34 @@ describe('SharedConnectorConfigurationStep', () => {
             expect(component.stepForm.get('enabled')?.value).toBe(false);
         });
 
+        // defaultValue and saved values are wire strings even for BOOLEAN. 
They must be coerced
+        // to real booleans, otherwise mat-slide-toggle's `!!value` renders 
"false" as checked.
+        it('coerces a BOOLEAN descriptor defaultValue of "false" to the 
boolean false', async () => {
+            const stepConfig = makeStepConfig('test-step', [
+                makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'false' })
+            ]);
+            const { component } = await setup({ stepConfig });
+            expect(component.stepForm.get('enabled')?.value).toBe(false);
+        });
+
+        it('coerces a BOOLEAN descriptor defaultValue of "true" to the boolean 
true', async () => {
+            const stepConfig = makeStepConfig('test-step', [
+                makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'true' })
+            ]);
+            const { component } = await setup({ stepConfig });
+            expect(component.stepForm.get('enabled')?.value).toBe(true);
+        });
+
+        it('coerces a saved BOOLEAN value of "false" to the boolean false', 
async () => {
+            const stepConfig = makeStepConfig(
+                'test-step',
+                [makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'true' 
})],
+                { enabled: { value: 'false', valueType: 'STRING_LITERAL' } }
+            );
+            const { component } = await setup({ stepConfig });
+            expect(component.stepForm.get('enabled')?.value).toBe(false);
+        });
+
         it('defaults STRING_LIST properties to empty array', async () => {
             const stepConfig = makeStepConfig('test-step', [makeProp('tags', { 
type: 'STRING_LIST' })]);
             const { component } = await setup({ stepConfig });
@@ -400,15 +428,53 @@ describe('SharedConnectorConfigurationStep', () => {
                     dependencies: [{ propertyName: 'mode', dependentValues: 
['advanced'] }]
                 })
             ]);
-            const { component } = await setup({ stepConfig });
+            const { component, fixture } = await setup({ stepConfig });
+            // setupFormSubscription is deferred with Promise.resolve() — same 
wait as the form-changes tests
+            await fixture.whenStable();
 
             component.setPropertyValue('mode', 'advanced');
-            component['computeAllPropertyVisibility']();
 
             
expect(component.stepForm.get('advanced-setting')?.enabled).toBe(true);
             
expect(component.isPropertyVisible(makeProp('advanced-setting'))).toBe(true);
         });
 
+        // A BOOLEAN toggle writes a native boolean into the form while 
dependentValues stays
+        // string[] from the API, so the comparison has to survive the type 
difference.
+        it('shows a property gated on a BOOLEAN once the toggle is turned on', 
async () => {
+            const stepConfig = makeStepConfig('test-step', [
+                makeProp('enableImageExtraction', { type: 'BOOLEAN', 
defaultValue: 'false' }),
+                makeProp('extractionMode', {
+                    dependencies: [{ propertyName: 'enableImageExtraction', 
dependentValues: ['true'] }]
+                })
+            ]);
+            const { component, fixture } = await setup({ stepConfig });
+            await fixture.whenStable();
+
+            
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(false);
+
+            component.setPropertyValue('enableImageExtraction', true);
+
+            
expect(component.stepForm.get('extractionMode')?.enabled).toBe(true);
+            
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(true);
+        });
+
+        it('hides a property gated on a BOOLEAN again once the toggle is 
turned back off', async () => {
+            const stepConfig = makeStepConfig('test-step', [
+                makeProp('enableImageExtraction', { type: 'BOOLEAN', 
defaultValue: 'true' }),
+                makeProp('extractionMode', {
+                    dependencies: [{ propertyName: 'enableImageExtraction', 
dependentValues: ['true'] }]
+                })
+            ]);
+            const { component, fixture } = await setup({ stepConfig });
+            await fixture.whenStable();
+
+            
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(true);
+
+            component.setPropertyValue('enableImageExtraction', false);
+
+            
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(false);
+        });
+
         it('hides a property when its dependency has no dependentValues and 
the parent is empty', async () => {
             const stepConfig = makeStepConfig('test-step', [
                 makeProp('optional-host'),
@@ -425,10 +491,10 @@ describe('SharedConnectorConfigurationStep', () => {
                 makeProp('optional-host'),
                 makeProp('host-port', { dependencies: [{ propertyName: 
'optional-host' }] })
             ]);
-            const { component } = await setup({ stepConfig });
+            const { component, fixture } = await setup({ stepConfig });
+            await fixture.whenStable();
 
             component.setPropertyValue('optional-host', 'myhost.com');
-            component['computeAllPropertyVisibility']();
 
             expect(component.stepForm.get('host-port')?.enabled).toBe(true);
         });
@@ -825,6 +891,42 @@ describe('SharedConnectorConfigurationStep', () => {
             const result = component.getConfigurationForSave();
             expect(result.isDirty).toBe(false);
         });
+
+        // initializeForm coerces BOOLEAN wire strings to real booleans. 
buildChangedConfiguration
+        // must use the same coercion when comparing against descriptor 
defaults, or an untouched
+        // "false" default would always appear changed (string "false" !== 
boolean false).
+        it('does not include an untouched BOOLEAN whose defaultValue was the 
wire string "false"', async () => {
+            const stepConfig = makeStepConfig('test-step', [
+                makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'false' 
}),
+                makeProp('host')
+            ]);
+            const { component } = await setup({ stepConfig });
+
+            component.stepForm.get('host')?.setValue('changed-host');
+            component.stepForm.markAsDirty();
+
+            const result = component.getConfigurationForSave();
+            const propertyValues = 
result.configuration?.propertyGroupConfigurations[0]?.propertyValues ?? {};
+            expect(propertyValues['enabled']).toBeUndefined();
+            expect(propertyValues['host']).toBeDefined();
+        });
+
+        it('does not include an untouched BOOLEAN whose saved value was the 
wire string "false"', async () => {
+            const stepConfig = makeStepConfig(
+                'test-step',
+                [makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'true' 
}), makeProp('host')],
+                { enabled: { value: 'false', valueType: 'STRING_LITERAL' } }
+            );
+            const { component } = await setup({ stepConfig });
+
+            component.stepForm.get('host')?.setValue('changed-host');
+            component.stepForm.markAsDirty();
+
+            const result = component.getConfigurationForSave();
+            const propertyValues = 
result.configuration?.propertyGroupConfigurations[0]?.propertyValues ?? {};
+            expect(propertyValues['enabled']).toBeUndefined();
+            expect(propertyValues['host']).toBeDefined();
+        });
     });
 
     // ═══════════════════════════════════════════════════════
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
index 35902ab1b04..dd2ba8a9c2d 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
@@ -47,7 +47,13 @@ import { StringListOrphansStrippedEvent } from 
'../../connector-property-input/c
 import { ErrorBanner } from '../../error-banner/error-banner.component';
 import { StatusBanner } from '../../status-banner/status-banner.component';
 import { StatusBannerDescriptionDirective } from 
'../../status-banner/status-banner.directives';
-import { fromValueReference, toValueReference, SecretReferenceOptions } from 
'../../../services/value-reference.helper';
+import {
+    fromValueReference,
+    toValueReference,
+    toBooleanValue,
+    SecretReferenceOptions
+} from '../../../services/value-reference.helper';
+import { isDependencyValueSatisfied } from 
'../../../utils/dependency-value.utils';
 import {
     AssetInfo,
     AssetReference,
@@ -472,7 +478,11 @@ export class SharedConnectorConfigurationStep implements 
SaveableStep, OnInit, O
                     const validators = this.buildValidators(property);
                     formConfig[property.name] = [formValue, validators];
                 } else {
-                    const currentValue = unsavedValue ?? apiValue ?? 
property.defaultValue ?? fallbackValue;
+                    const resolvedValue = unsavedValue ?? apiValue ?? 
property.defaultValue ?? fallbackValue;
+                    // The descriptor's defaultValue is a wire string even for 
BOOLEAN, so coerce
+                    // before it enters the form tree: the toggle renders 
`!!value` and would show
+                    // a stored "false" as checked.
+                    const currentValue = property.type === 'BOOLEAN' ? 
toBooleanValue(resolvedValue) : resolvedValue;
                     const validators = this.buildValidators(property);
                     formConfig[property.name] = [currentValue, validators];
                 }
@@ -648,13 +658,7 @@ export class SharedConnectorConfigurationStep implements 
SaveableStep, OnInit, O
 
             const dependentValue = dependentControl.value;
 
-            if (dependency.dependentValues && 
dependency.dependentValues.length > 0) {
-                // If specific values are required, check if current value 
matches
-                return dependency.dependentValues.includes(dependentValue as 
string);
-            } else {
-                // If no specific values, just check if dependent property has 
any value
-                return dependentValue !== null && dependentValue !== undefined 
&& dependentValue !== '';
-            }
+            return isDependencyValueSatisfied(dependentValue, 
dependency.dependentValues);
         });
     }
 
@@ -898,8 +902,12 @@ export class SharedConnectorConfigurationStep implements 
SaveableStep, OnInit, O
 
                             let originalValue = apiValue ?? 
property.defaultValue ?? fallbackValue;
 
-                            // Normalize assets so comparison matches the form 
value shape
-                            if (property.type === 'ASSET') {
+                            // Normalize so comparison matches the form value 
shape
+                            if (property.type === 'BOOLEAN') {
+                                // initializeForm coerces BOOLEAN values to 
real booleans; without the
+                                // same coercion here a stored "false" would 
always look changed.
+                                originalValue = toBooleanValue(originalValue);
+                            } else if (property.type === 'ASSET') {
                                 // fromValueReference returns 
AssetReference|null; form stores string|null
                                 originalValue = originalValue?.id ?? null;
                             } else if (property.type === 'ASSET_LIST') {
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
index 8379fdce2d3..9205c79aaf5 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
@@ -121,6 +121,30 @@ describe('Step Dependency Utils', () => {
 
             expect(isStepDependencySatisfied(dependency, {}, {}, 
visibleSteps)).toBe(false);
         });
+
+        // A BOOLEAN toggle puts a native boolean into the form, while 
dependentValues is always
+        // string[] from the API. Both directions of the toggle must be 
evaluated correctly.
+        it('should satisfy a boolean dependency when the unsaved value is the 
boolean true', () => {
+            const dependency = { stepName: 'Step1', propertyName: 
'enableCortex', dependentValues: ['true'] };
+            const stepConfigurations = {
+                Step1: createStepConfig('Step1', [], { group1: { enableCortex: 
'false' } })
+            };
+            const unsavedValues = { Step1: { enableCortex: true } };
+            const visibleSteps = new Set(['Step1']);
+
+            expect(isStepDependencySatisfied(dependency, stepConfigurations, 
unsavedValues, visibleSteps)).toBe(true);
+        });
+
+        it('should not satisfy a boolean dependency when the unsaved value is 
the boolean false', () => {
+            const dependency = { stepName: 'Step1', propertyName: 
'enableCortex', dependentValues: ['true'] };
+            const stepConfigurations = {
+                Step1: createStepConfig('Step1', [], { group1: { enableCortex: 
'true' } })
+            };
+            const unsavedValues = { Step1: { enableCortex: false } };
+            const visibleSteps = new Set(['Step1']);
+
+            expect(isStepDependencySatisfied(dependency, stepConfigurations, 
unsavedValues, visibleSteps)).toBe(false);
+        });
     });
 
     describe('getVisibleStepNames', () => {
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
index 696b596f013..be0f84a4ef0 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
@@ -17,6 +17,7 @@
 
 import { ConfigurationStepConfiguration, ConfigurationStepDependency, 
ConnectorPropertyFormValue } from '../../types';
 import { fromValueReference } from '../../services/value-reference.helper';
+import { isDependencyValueSatisfied } from 
'../../utils/dependency-value.utils';
 
 /**
  * Get the current value of a property from a step.
@@ -87,14 +88,7 @@ export function isStepDependencySatisfied(
     // Get the current property value
     const value = getPropertyValue(stepName, propertyName, stepConfigurations, 
unsavedStepValues);
 
-    // Evaluate based on dependentValues
-    if (!dependentValues || dependentValues.length === 0) {
-        // No specific values required - any non-empty value satisfies
-        return value !== null && value !== undefined && value !== '';
-    } else {
-        // Value must be in the allowed list
-        return value != null && dependentValues.includes(String(value));
-    }
+    return isDependencyValueSatisfied(value, dependentValues);
 }
 
 /**
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
index b0b39c907eb..da84c658280 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
@@ -146,6 +146,18 @@ describe('PropertyGroupCard', () => {
             expect(component.hasValue('Host')).toBe(false);
         });
 
+        it('should return true when no value reference exists but the 
descriptor has a default', async () => {
+            const { component } = await setup({
+                propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Enabled: { name: 'Enabled', type: 'BOOLEAN', required: 
false, defaultValue: 'false' }
+                    },
+                    propertyValues: {}
+                })
+            });
+            expect(component.hasValue('Enabled')).toBe(true);
+        });
+
         it('should return false for STRING_LITERAL with null value', async () 
=> {
             const { component } = await setup({
                 propertyGroup: makeGroup({
@@ -168,40 +180,66 @@ describe('PropertyGroupCard', () => {
             expect(component.hasValue('Host')).toBe(false);
         });
 
-        it('should return true for SECRET_REFERENCE regardless of value', 
async () => {
+        it('should return true for a SECRET_REFERENCE with a non-empty 
composite key', async () => {
             const { component } = await setup({
                 propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Password: { name: 'Password', type: 'SECRET', 
required: true }
+                    },
                     propertyValues: {
-                        Host: { valueType: 'SECRET_REFERENCE' }
+                        Password: {
+                            valueType: 'SECRET_REFERENCE',
+                            secretProviderId: 'provider-1',
+                            secretProviderName: 'Vault',
+                            fullyQualifiedSecretName: 'group.my-secret'
+                        }
                     }
                 })
             });
-            expect(component.hasValue('Host')).toBe(true);
+            expect(component.hasValue('Password')).toBe(true);
+        });
+
+        it('should return false for a SECRET with only a descriptor default 
and no saved reference', async () => {
+            const { component } = await setup({
+                propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Password: { name: 'Password', type: 'SECRET', 
required: true, defaultValue: 'changeme' }
+                    },
+                    propertyValues: {}
+                })
+            });
+            expect(component.hasValue('Password')).toBe(false);
         });
 
-        it('should return true for ASSET_REFERENCE with entries', async () => {
+        it('should return true for an ASSET with assetReferences', async () => 
{
             const { component } = await setup({
                 propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Cert: { name: 'Cert', type: 'ASSET', required: false }
+                    },
                     propertyValues: {
-                        Host: {
+                        Cert: {
                             valueType: 'ASSET_REFERENCE',
                             assetReferences: [{ id: 'asset-1', name: 
'cert.pem' }]
                         }
                     }
                 })
             });
-            expect(component.hasValue('Host')).toBe(true);
+            expect(component.hasValue('Cert')).toBe(true);
         });
 
-        it('should return false for ASSET_REFERENCE with empty array', async 
() => {
+        it('should return false for an ASSET with empty assetReferences', 
async () => {
             const { component } = await setup({
                 propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Cert: { name: 'Cert', type: 'ASSET', required: false }
+                    },
                     propertyValues: {
-                        Host: { valueType: 'ASSET_REFERENCE', assetReferences: 
[] }
+                        Cert: { valueType: 'ASSET_REFERENCE', assetReferences: 
[] }
                     }
                 })
             });
-            expect(component.hasValue('Host')).toBe(false);
+            expect(component.hasValue('Cert')).toBe(false);
         });
     });
 
@@ -214,19 +252,42 @@ describe('PropertyGroupCard', () => {
         it('should return masked text for SECRET_REFERENCE', async () => {
             const { component } = await setup({
                 propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Password: { name: 'Password', type: 'SECRET', 
required: true }
+                    },
                     propertyValues: {
-                        Host: { valueType: 'SECRET_REFERENCE' }
+                        Password: {
+                            valueType: 'SECRET_REFERENCE',
+                            secretProviderId: 'provider-1',
+                            secretProviderName: 'Vault',
+                            fullyQualifiedSecretName: 'group.my-secret'
+                        }
                     }
                 })
             });
-            
expect(component.getDisplayValueForProperty('Host')).toBe('••••••••');
+            
expect(component.getDisplayValueForProperty('Password')).toBe('••••••••');
+        });
+
+        it('should not leak a SECRET descriptor default as visible text', 
async () => {
+            const { component } = await setup({
+                propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Password: { name: 'Password', type: 'SECRET', 
required: true, defaultValue: 'changeme' }
+                    },
+                    propertyValues: {}
+                })
+            });
+            
expect(component.getDisplayValueForProperty('Password')).toBe('••••••••');
         });
 
         it('should return comma-separated asset names for ASSET_REFERENCE', 
async () => {
             const { component } = await setup({
                 propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Cert: { name: 'Cert', type: 'ASSET', required: false }
+                    },
                     propertyValues: {
-                        Host: {
+                        Cert: {
                             valueType: 'ASSET_REFERENCE',
                             assetReferences: [
                                 { id: 'a1', name: 'cert.pem' },
@@ -236,21 +297,24 @@ describe('PropertyGroupCard', () => {
                     }
                 })
             });
-            
expect(component.getDisplayValueForProperty('Host')).toBe('cert.pem, key.pem');
+            
expect(component.getDisplayValueForProperty('Cert')).toBe('cert.pem, key.pem');
         });
 
         it('should fall back to asset id when name is missing', async () => {
             const { component } = await setup({
                 propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Cert: { name: 'Cert', type: 'ASSET', required: false }
+                    },
                     propertyValues: {
-                        Host: {
+                        Cert: {
                             valueType: 'ASSET_REFERENCE',
                             assetReferences: [{ id: 'a1' }]
                         }
                     }
                 })
             });
-            expect(component.getDisplayValueForProperty('Host')).toBe('a1');
+            expect(component.getDisplayValueForProperty('Cert')).toBe('a1');
         });
 
         it('should return empty string when no value reference exists', async 
() => {
@@ -259,6 +323,18 @@ describe('PropertyGroupCard', () => {
             });
             expect(component.getDisplayValueForProperty('Host')).toBe('');
         });
+
+        it('should return the descriptor default when no value reference 
exists', async () => {
+            const { component } = await setup({
+                propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Enabled: { name: 'Enabled', type: 'BOOLEAN', required: 
false, defaultValue: 'false' }
+                    },
+                    propertyValues: {}
+                })
+            });
+            
expect(component.getDisplayValueForProperty('Enabled')).toBe('false');
+        });
     });
 
     describe('fieldErrors computed signal', () => {
@@ -348,6 +424,19 @@ describe('PropertyGroupCard', () => {
             expect(unsetLabels[0].nativeElement.textContent.trim()).toBe('No 
value set');
         });
 
+        it('should display descriptor defaults when property values are 
absent', async () => {
+            const { query } = await setup({
+                propertyGroup: makeGroup({
+                    propertyDescriptors: {
+                        Enabled: { name: 'Enabled', type: 'BOOLEAN', required: 
false, defaultValue: 'false' }
+                    },
+                    propertyValues: {}
+                })
+            });
+            const value = 
query('mat-card-content').query(By.css('.tertiary-color'));
+            expect(value.nativeElement.textContent.trim()).toBe('false');
+        });
+
         it('should display the property value for properties with values', 
async () => {
             const { query } = await setup();
             const content = query('mat-card-content');
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
index 188d73ba8be..bd7b093dda1 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
@@ -25,6 +25,7 @@ import {
     ConnectorPropertyDescriptor,
     PropertyGroupConfiguration
 } from '../../types';
+import { hasPropertyValue } from '../../utils/connector-validation.utils';
 
 /**
  * Read-only display card for a property group's configured values.
@@ -74,22 +75,21 @@ export class PropertyGroupCard {
 
     hasValue(propertyName: string): boolean {
         const valueRef = this.propertyGroup().propertyValues?.[propertyName];
-        if (!valueRef) return false;
-        if (valueRef.valueType === 'SECRET_REFERENCE') return true;
-        if (valueRef.valueType === 'ASSET_REFERENCE') {
-            return (valueRef.assetReferences?.length ?? 0) > 0;
-        }
-        return valueRef.value !== null && valueRef.value !== undefined && 
valueRef.value !== '';
+        const descriptor = this.getDescriptor(propertyName);
+        return hasPropertyValue(valueRef, descriptor?.type ?? 'STRING', 
descriptor?.defaultValue);
     }
 
     getDisplayValueForProperty(propertyName: string): string {
         const valueRef = this.propertyGroup().propertyValues?.[propertyName];
-        if (!valueRef) return '';
-        if (valueRef.valueType === 'SECRET_REFERENCE') return '••••••••';
-        if (valueRef.valueType === 'ASSET_REFERENCE') {
+        const descriptor = this.getDescriptor(propertyName);
+
+        // Mask by type, not just a saved SECRET_REFERENCE, so a SECRET 
defaultValue cannot leak.
+        if (descriptor?.type === 'SECRET' || valueRef?.valueType === 
'SECRET_REFERENCE') return '••••••••';
+        if (valueRef?.valueType === 'ASSET_REFERENCE') {
             const refs = valueRef.assetReferences;
             return refs?.map((r: AssetReference) => r.name || r.id).join(', ') 
|| '';
         }
-        return valueRef.value ?? '';
+
+        return valueRef?.value ?? descriptor?.defaultValue ?? '';
     }
 }
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.spec.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.spec.ts
new file mode 100644
index 00000000000..df5894ea5a6
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fromValueReference, toBooleanValue } from './value-reference.helper';
+
+describe('Value Reference Helper', () => {
+    describe('toBooleanValue', () => {
+        it('coerces true values case-insensitively to match 
Boolean.parseBoolean', () => {
+            expect(toBooleanValue(true)).toBe(true);
+            expect(toBooleanValue('true')).toBe(true);
+            expect(toBooleanValue('True')).toBe(true);
+            expect(toBooleanValue('TRUE')).toBe(true);
+        });
+
+        it('coerces all other values to false', () => {
+            expect(toBooleanValue(false)).toBe(false);
+            expect(toBooleanValue('false')).toBe(false);
+            expect(toBooleanValue('anything')).toBe(false);
+            expect(toBooleanValue(null)).toBe(false);
+            expect(toBooleanValue(undefined)).toBe(false);
+        });
+    });
+
+    describe('fromValueReference', () => {
+        it('normalizes BOOLEAN string values case-insensitively', () => {
+            expect(fromValueReference({ value: 'TRUE', valueType: 
'STRING_LITERAL' }, 'BOOLEAN')).toBe(true);
+            expect(fromValueReference({ value: 'false', valueType: 
'STRING_LITERAL' }, 'BOOLEAN')).toBe(false);
+        });
+
+        it('preserves a null BOOLEAN value so callers can use the descriptor 
default', () => {
+            expect(fromValueReference({ value: null, valueType: 
'STRING_LITERAL' }, 'BOOLEAN')).toBeNull();
+        });
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
index e0450edca68..5b677403167 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
@@ -37,6 +37,20 @@ export interface SecretReferenceOptions {
     fullyQualifiedSecretName: string;
 }
 
+/**
+ * Coerces a BOOLEAN property value into a real boolean.
+ *
+ * BOOLEAN values cross the wire as strings (both
+ * `ConnectorValueReferenceDTO.value` and 
`ConnectorPropertyDescriptorDTO.defaultValue`
+ * are declared as String server-side). Comparison is case-insensitive to match
+ * `Boolean.parseBoolean`, which the connector framework uses when reading 
BOOLEAN values.
+ * `MatSlideToggle.writeValue` coerces with a bare `!!value`, so the string 
`"false"` is truthy
+ * and would otherwise render the toggle as checked.
+ */
+export function toBooleanValue(value: unknown): boolean {
+    return value === true || (typeof value === 'string' && value.toLowerCase() 
=== 'true');
+}
+
 /**
  * Creates a ConnectorValueReference from a primitive form value.
  * Supports STRING_LITERAL, ASSET_REFERENCE, and SECRET_REFERENCE value types.
@@ -127,7 +141,7 @@ export function toValueReference(
  * for use with multi-select form controls.
  *
  * @param valueRef The ConnectorValueReference from the API, or a plain 
primitive value
- * @param propertyType Optional property type - when 'STRING_LIST', splits 
comma-separated values into array
+ * @param propertyType Optional property type used to normalize BOOLEAN, 
STRING_LIST, and ASSET values
  * @returns The primitive value suitable for display
  */
 export function fromValueReference(
@@ -196,6 +210,13 @@ export function fromValueReference(
         return [];
     }
 
+    // For BOOLEAN, coerce the wire string ("true"/"false") into a real 
boolean so the
+    // toggle renders the stored value rather than the truthiness of a 
non-empty string.
+    // null/undefined is preserved so callers can still fall back to the 
descriptor default.
+    if (propertyType === 'BOOLEAN' && rawValue !== null && rawValue !== 
undefined) {
+        return toBooleanValue(rawValue);
+    }
+
     // For STRING_LIST, split comma-separated string into array for 
multi-select
     if (propertyType === 'STRING_LIST' && typeof rawValue === 'string') {
         return rawValue
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
index 04ee9bccfa6..15895d54f7b 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
@@ -23,6 +23,7 @@ import {
     PropertyType,
     buildSecretKey
 } from '../types';
+import { isDependencyValueSatisfied } from './dependency-value.utils';
 
 /**
  * Check if a property has a meaningful value set.
@@ -139,10 +140,7 @@ function evaluatePropertyVisibility(
 
         const dependentValue = findPropertyValue(dependency.propertyName, 
propertyGroups);
 
-        if (dependency.dependentValues && dependency.dependentValues.length > 
0) {
-            return dependentValue !== null && 
dependency.dependentValues.includes(dependentValue);
-        }
-        return dependentValue !== null && dependentValue !== '';
+        return isDependencyValueSatisfied(dependentValue, 
dependency.dependentValues);
     });
 }
 
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.spec.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.spec.ts
new file mode 100644
index 00000000000..e24b9d04018
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { isDependencyValueSatisfied } from './dependency-value.utils';
+
+describe('Dependency Value Utils', () => {
+    it('matches native booleans against string dependent values', () => {
+        expect(isDependencyValueSatisfied(true, ['true'])).toBe(true);
+        expect(isDependencyValueSatisfied(false, ['true'])).toBe(false);
+        expect(isDependencyValueSatisfied(false, ['false'])).toBe(true);
+    });
+
+    it('matches string values case-sensitively', () => {
+        expect(isDependencyValueSatisfied('true', ['true'])).toBe(true);
+        expect(isDependencyValueSatisfied('TRUE', ['true'])).toBe(false);
+        expect(isDependencyValueSatisfied('value2', ['value1', 
'value2'])).toBe(true);
+    });
+
+    it('treats any non-empty value as satisfying an empty dependent values 
list', () => {
+        expect(isDependencyValueSatisfied('anything', [])).toBe(true);
+        expect(isDependencyValueSatisfied(false, undefined)).toBe(true);
+        expect(isDependencyValueSatisfied('', [])).toBe(false);
+        expect(isDependencyValueSatisfied(null, [])).toBe(false);
+        expect(isDependencyValueSatisfied(undefined, [])).toBe(false);
+    });
+
+    it('does not match null or undefined against a dependent values list', () 
=> {
+        expect(isDependencyValueSatisfied(null, ['true'])).toBe(false);
+        expect(isDependencyValueSatisfied(undefined, 
['undefined'])).toBe(false);
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.ts
new file mode 100644
index 00000000000..f7fcee4fe33
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.ts
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ConnectorPropertyFormValue } from '../types';
+
+/**
+ * Evaluates whether a dependency condition is met by a property's current 
value.
+ *
+ * The API represents dependent values as strings, while live form values can 
be native
+ * booleans. Dependency matching remains case-sensitive to match backend 
dependency evaluators.
+ *
+ * @param value The dependent property's current value
+ * @param dependentValues Values that satisfy the dependency; empty means any 
non-empty value
+ * @returns whether the dependency condition is met
+ */
+export function isDependencyValueSatisfied(
+    value: ConnectorPropertyFormValue | undefined,
+    dependentValues: string[] | undefined | null
+): boolean {
+    if (!dependentValues || dependentValues.length === 0) {
+        return value !== null && value !== undefined && value !== '';
+    }
+
+    return value !== null && value !== undefined && 
dependentValues.includes(String(value));
+}

Reply via email to