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

rfellows 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 4c052994bd NIFI-14988: Fixing issue with default values for dynamic 
properties. (#10320)
4c052994bd is described below

commit 4c052994bd67d64ee829849db198e42c972017c7
Author: Matt Gilman <[email protected]>
AuthorDate: Thu Sep 18 11:27:55 2025 -0400

    NIFI-14988: Fixing issue with default values for dynamic properties. 
(#10320)
    
    - In the combo editor, the form was not marked as dirty which caused the 
Apply button to not be enabled.
    - In the nf editor, the default value was not considered.
    
    This closes #10320
---
 .../combo-editor/combo-editor.component.spec.ts    |  70 +++++++
 .../editors/combo-editor/combo-editor.component.ts | 219 +++++++++++----------
 .../editors/nf-editor/nf-editor.component.spec.ts  |  58 ++++++
 .../editors/nf-editor/nf-editor.component.ts       |  17 +-
 4 files changed, 261 insertions(+), 103 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
index 6f472d13cc..ecbc01d03c 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
@@ -177,6 +177,76 @@ describe('ComboEditor', () => {
         }
     });
 
+    it('verify form is dirty when using default value', () => {
+        if (item) {
+            item.value = null;
+            item.descriptor.required = false;
+            item.savedValue = 'some-other-value';
+
+            component.item = item;
+            component.parameterConfig = {
+                supportsParameters: false,
+                parameters: null
+            };
+
+            fixture.detectChanges();
+
+            // Form should be dirty because we're using the default value 
instead of the saved value
+            expect(component.comboEditorForm.dirty).toBe(true);
+            
expect(component.configuredValue).toEqual(item.descriptor.defaultValue);
+        }
+    });
+
+    it('verify input order independence - item first then parameterConfig', () 
=> {
+        if (item) {
+            item.value = null;
+            item.descriptor.required = false;
+            item.savedValue = 'some-other-value';
+
+            // Set item first
+            component.item = item;
+
+            // Set parameterConfig second
+            component.parameterConfig = {
+                supportsParameters: true,
+                parameters
+            };
+
+            fixture.detectChanges();
+
+            // Should work correctly regardless of input order
+            expect(component.comboEditorForm.dirty).toBe(true);
+            
expect(component.configuredValue).toEqual(item.descriptor.defaultValue);
+            expect(component.supportsParameters).toBe(true);
+            expect(component.parameters).toBe(parameters);
+        }
+    });
+
+    it('verify input order independence - parameterConfig first then item', () 
=> {
+        if (item) {
+            item.value = null;
+            item.descriptor.required = false;
+            item.savedValue = 'some-other-value';
+
+            // Set parameterConfig first
+            component.parameterConfig = {
+                supportsParameters: true,
+                parameters
+            };
+
+            // Set item second
+            component.item = item;
+
+            fixture.detectChanges();
+
+            // Should work correctly regardless of input order
+            expect(component.comboEditorForm.dirty).toBe(true);
+            
expect(component.configuredValue).toEqual(item.descriptor.defaultValue);
+            expect(component.supportsParameters).toBe(true);
+            expect(component.parameters).toBe(parameters);
+        }
+    });
+
     it('verify combo not required with null value and no default', () => {
         if (item) {
             item.value = null;
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
index 37ffb08ef7..ece7c9f794 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
@@ -62,6 +62,8 @@ export class ComboEditor {
             this.configuredValue = item.value;
         } else if (item.descriptor.defaultValue != null) {
             this.configuredValue = item.descriptor.defaultValue;
+            // Mark as dirty since we're using default value instead of the 
saved value
+            this.shouldMarkDirty = true;
         }
 
         this.descriptor = item.descriptor;
@@ -69,13 +71,14 @@ export class ComboEditor {
         this.savedValue = item.savedValue;
 
         this.itemSet = true;
-        this.initialAllowableValues();
+        this.initializeComponent();
     }
 
     @Input() set parameterConfig(parameterConfig: ParameterConfig) {
         this.parameters = parameterConfig.parameters;
         this.supportsParameters = parameterConfig.supportsParameters;
-        this.initialAllowableValues();
+        this.parameterConfigSet = true;
+        this.initializeComponent();
     }
     @Input() width!: number;
     @Input() readonly: boolean = false;
@@ -100,6 +103,8 @@ export class ComboEditor {
     supportsParameters = false;
 
     itemSet = false;
+    parameterConfigSet = false;
+    shouldMarkDirty = false;
     configuredValue: string | null = null;
     savedValue: string | null = null;
     parameters: Parameter[] | null = null;
@@ -113,116 +118,128 @@ export class ComboEditor {
         });
     }
 
-    initialAllowableValues(): void {
-        if (this.itemSet) {
-            this.itemLookup.clear();
-            this.allowableValues = [];
-            this.referencesParametersId = -1;
-
-            let i = 0;
-            let selectedItem: AllowableValueItem | null = null;
-
-            if (!this.descriptor.required) {
-                const noValue: AllowableValueItem = {
-                    id: i++,
-                    disabled: false,
-                    displayName: 'No value',
-                    value: null
-                };
-                this.itemLookup.set(noValue.id, noValue);
-                this.allowableValues.push(noValue);
-
-                if (noValue.value == this.configuredValue) {
-                    selectedItem = noValue;
-                }
+    private initializeComponent(): void {
+        // Only initialize when both required inputs are set
+        if (this.itemSet && this.parameterConfigSet) {
+            this.initializeAllowableValues();
+
+            // Mark the form as dirty if we used a default value
+            if (this.shouldMarkDirty) {
+                this.comboEditorForm.markAsDirty();
+                this.shouldMarkDirty = false; // Reset flag
             }
+        }
+    }
 
-            if (this.descriptor.allowableValues) {
-                const allowableValueItems: AllowableValueItem[] = 
this.descriptor.allowableValues.map(
-                    (allowableValueEntity) => {
-                        const allowableValue: AllowableValueItem = {
-                            ...allowableValueEntity.allowableValue,
-                            id: i++,
-                            disabled:
-                                !allowableValueEntity.canRead &&
-                                allowableValueEntity.allowableValue.value !== 
this.savedValue
-                        };
-                        this.itemLookup.set(allowableValue.id, allowableValue);
-
-                        if (allowableValue.value == this.configuredValue) {
-                            selectedItem = allowableValue;
-                        }
+    private initializeAllowableValues(): void {
+        this.itemLookup.clear();
+        this.allowableValues = [];
+        this.parameterAllowableValues = [];
+        this.referencesParametersId = -1;
+
+        let i = 0;
+        let selectedItem: AllowableValueItem | null = null;
+
+        if (!this.descriptor.required) {
+            const noValue: AllowableValueItem = {
+                id: i++,
+                disabled: false,
+                displayName: 'No value',
+                value: null
+            };
+            this.itemLookup.set(noValue.id, noValue);
+            this.allowableValues.push(noValue);
+
+            if (noValue.value == this.configuredValue) {
+                selectedItem = noValue;
+            }
+        }
 
-                        return allowableValue;
+        if (this.descriptor.allowableValues) {
+            const allowableValueItems: AllowableValueItem[] = 
this.descriptor.allowableValues.map(
+                (allowableValueEntity) => {
+                    const allowableValue: AllowableValueItem = {
+                        ...allowableValueEntity.allowableValue,
+                        id: i++,
+                        disabled:
+                            !allowableValueEntity.canRead &&
+                            allowableValueEntity.allowableValue.value !== 
this.savedValue
+                    };
+                    this.itemLookup.set(allowableValue.id, allowableValue);
+
+                    if (allowableValue.value == this.configuredValue) {
+                        selectedItem = allowableValue;
                     }
-                );
-                this.allowableValues.push(...allowableValueItems);
-            }
 
-            if (this.supportsParameters) {
-                // parameters are supported so add the item to support showing
-                // and hiding the parameter options select
-                const referencesParameterOption: AllowableValueItem = {
-                    id: i++,
-                    disabled: false,
-                    displayName: 'Reference Parameter...',
-                    value: null
-                };
-                this.allowableValues.push(referencesParameterOption);
-                this.itemLookup.set(referencesParameterOption.id, 
referencesParameterOption);
-
-                // record the item of the item to more easily identify this 
item
-                this.referencesParametersId = referencesParameterOption.id;
-
-                // if the current value references a parameter auto select the
-                // references parameter item
-                if (this.referencesParameter(this.configuredValue)) {
-                    selectedItem = referencesParameterOption;
-
-                    // trigger allowable value changed to show the parameters
-                    this.allowableValueChanged(this.referencesParametersId);
+                    return allowableValue;
                 }
+            );
+            this.allowableValues.push(...allowableValueItems);
+        }
 
-                if (this.parameters !== null && this.parameters.length > 0) {
-                    // capture the value of i which will be the id of the first
-                    // parameter
-                    this.configuredParameterId = i;
-
-                    // create allowable values for each parameter
-                    this.parameters.forEach((parameter) => {
-                        const parameterItem: AllowableValueItem = {
-                            id: i++,
-                            disabled: false,
-                            displayName: parameter.name,
-                            value: `#{${parameter.name}}`,
-                            description: parameter.description
-                        };
-                        this.parameterAllowableValues.push(parameterItem);
-                        this.itemLookup.set(parameterItem.id, parameterItem);
-
-                        // if the configured parameter is still available,
-                        // capture the id, so we can auto select it
-                        if (parameterItem.value === this.configuredValue) {
-                            this.configuredParameterId = parameterItem.id;
-                        }
-                    });
-                    this.parameterAllowableValues.sort((a, b) =>
-                        this.nifiCommon.compareString(a.displayName, 
b.displayName)
-                    );
-                    // if combo still set to reference a parameter, set the 
default value
-                    if (selectedItem?.id == this.referencesParametersId) {
-                        
this.comboEditorForm.get('parameterReference')?.setValue(this.configuredParameterId);
+        if (this.supportsParameters) {
+            // parameters are supported so add the item to support showing
+            // and hiding the parameter options select
+            const referencesParameterOption: AllowableValueItem = {
+                id: i++,
+                disabled: false,
+                displayName: 'Reference Parameter...',
+                value: null
+            };
+            this.allowableValues.push(referencesParameterOption);
+            this.itemLookup.set(referencesParameterOption.id, 
referencesParameterOption);
+
+            // record the item of the item to more easily identify this item
+            this.referencesParametersId = referencesParameterOption.id;
+
+            // if the current value references a parameter auto select the
+            // references parameter item
+            if (this.referencesParameter(this.configuredValue)) {
+                selectedItem = referencesParameterOption;
+
+                // trigger allowable value changed to show the parameters
+                this.allowableValueChanged(this.referencesParametersId);
+            }
+
+            if (this.parameters !== null && this.parameters.length > 0) {
+                // capture the value of i which will be the id of the first
+                // parameter
+                this.configuredParameterId = i;
+
+                // create allowable values for each parameter
+                this.parameters.forEach((parameter) => {
+                    const parameterItem: AllowableValueItem = {
+                        id: i++,
+                        disabled: false,
+                        displayName: parameter.name,
+                        value: `#{${parameter.name}}`,
+                        description: parameter.description
+                    };
+                    this.parameterAllowableValues.push(parameterItem);
+                    this.itemLookup.set(parameterItem.id, parameterItem);
+
+                    // if the configured parameter is still available,
+                    // capture the id, so we can auto select it
+                    if (parameterItem.value === this.configuredValue) {
+                        this.configuredParameterId = parameterItem.id;
                     }
+                });
+                this.parameterAllowableValues.sort((a, b) =>
+                    this.nifiCommon.compareString(a.displayName, b.displayName)
+                );
+                // if combo still set to reference a parameter, set the 
default value
+                if (selectedItem?.id == this.referencesParametersId) {
+                    
this.comboEditorForm.get('parameterReference')?.setValue(this.configuredParameterId);
                 }
-            } else {
-                this.parameterAllowableValues = [];
             }
+        } else {
+            this.parameterAllowableValues = [];
+        }
 
-            if (selectedItem) {
-                // mat-select does not have good support for options with null 
value so we've
-                // introduced a mapping to work around the shortcoming
-                this.comboEditorForm.get('value')?.setValue(selectedItem.id);
-            }
+        if (selectedItem) {
+            // mat-select does not have good support for options with null 
value so we've
+            // introduced a mapping to work around the shortcoming
+            this.comboEditorForm.get('value')?.setValue(selectedItem.id);
         }
     }
 
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
index e252af4679..7235b838b0 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
@@ -181,4 +181,62 @@ describe('NfEditor', () => {
         
expect(mockNifiLanguagePackage.isValidParameter).toHaveBeenCalledWith('testParam');
         
expect(mockNifiLanguagePackage.isValidElFunction).toHaveBeenCalledWith('uuid');
     });
+
+    it('should use default value and mark form dirty when item value is null', 
() => {
+        const mockItem: PropertyItem = {
+            property: 'test.property',
+            descriptor: {
+                name: 'test.property',
+                displayName: 'Test Property',
+                description: 'A test property',
+                required: false,
+                sensitive: false,
+                dynamic: false,
+                supportsEl: false,
+                expressionLanguageScope: 'NONE',
+                dependencies: [],
+                defaultValue: 'default-test-value'
+            },
+            value: null,
+            id: 1,
+            triggerEdit: false,
+            deleted: false,
+            added: false,
+            dirty: false,
+            savedValue: 'some-other-value',
+            type: 'optional'
+        };
+
+        // Mock the form controls to track setValue and markAsDirty calls
+        const mockValueControl = {
+            setValue: jest.fn(),
+            addValidators: jest.fn(),
+            removeValidators: jest.fn(),
+            disable: jest.fn(),
+            enable: jest.fn()
+        };
+        const mockEmptyStringControl = {
+            setValue: jest.fn(),
+            value: false
+        };
+        const mockForm = {
+            get: jest.fn((control: string) => {
+                if (control === 'value') return mockValueControl;
+                if (control === 'setEmptyString') return 
mockEmptyStringControl;
+                return null;
+            }),
+            markAsDirty: jest.fn()
+        };
+
+        component.nfEditorForm = mockForm as any;
+        component.item = mockItem;
+        component.parameterConfig = {
+            parameters: null,
+            supportsParameters: false
+        };
+
+        // Verify that the default value was set and form was marked dirty
+        
expect(mockValueControl.setValue).toHaveBeenCalledWith('default-test-value');
+        expect(mockForm.markAsDirty).toHaveBeenCalled();
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
index 2fecbf3846..32bc99245f 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
@@ -67,11 +67,19 @@ import { completionKeymap } from '@codemirror/autocomplete';
 })
 export class NfEditor {
     @Input() set item(item: PropertyItem) {
+        let shouldMarkDirty = false;
+        let valueToSet = item.value;
+
         if (item.descriptor.sensitive && item.value !== null) {
             this.nfEditorForm.get('value')?.setValue('Sensitive value set');
             this.showSensitiveHelperText = true;
         } else {
-            this.nfEditorForm.get('value')?.setValue(item.value);
+            // If the current value is null but there's a default value, use 
the default
+            if (item.value == null && item.descriptor.defaultValue != null) {
+                valueToSet = item.descriptor.defaultValue;
+                shouldMarkDirty = true;
+            }
+            this.nfEditorForm.get('value')?.setValue(valueToSet);
         }
 
         if (item.descriptor.required) {
@@ -80,7 +88,7 @@ export class NfEditor {
             
this.nfEditorForm.get('value')?.removeValidators(Validators.required);
         }
 
-        const isEmptyString: boolean = item.value === '';
+        const isEmptyString: boolean = (valueToSet || item.value) === '';
         this.nfEditorForm.get('setEmptyString')?.setValue(isEmptyString);
         this.setEmptyStringChanged();
 
@@ -88,6 +96,11 @@ export class NfEditor {
         this.itemSet = true;
 
         this.initializeCodeMirror();
+
+        // Mark the form as dirty if we used a default value
+        if (shouldMarkDirty) {
+            this.nfEditorForm.markAsDirty();
+        }
     }
 
     @Input() set parameterConfig(parameterConfig: ParameterConfig) {

Reply via email to