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

scottyaslan 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 143ba4a634d NIFI-16240 - Navigate to the referenced parameter from 
property table Go to Parameter (#11580)
143ba4a634d is described below

commit 143ba4a634d9c7dec56890491e9e55e6b5755e58
Author: Rob Fellows <[email protected]>
AuthorDate: Wed Aug 26 15:22:40 2026 -0400

    NIFI-16240 - Navigate to the referenced parameter from property table Go to 
Parameter (#11580)
    
    * NIFI-16240 - Navigate to the referenced parameter from property table Go 
to Parameter
    
    Extract the referenced parameter name from a property value and pass it 
through
    router state so the Parameter Context edit dialog can highlight and scroll 
to
    that row. Carry the same state through save-then-navigate so the highlight 
is
    preserved when the edit dialog is dirty.
    
    * address review feedback
---
 .../controller-services.effects.spec.ts            | 184 ++++++++++++++++++-
 .../controller-services.effects.ts                 |  41 ++++-
 .../state/controller-services/index.ts             |   4 +-
 .../flow-designer/state/flow/flow.effects.spec.ts  | 194 ++++++++++++++++++++-
 .../pages/flow-designer/state/flow/flow.effects.ts |  35 +++-
 .../apps/nifi/src/app/state/shared/index.ts        |   3 +
 .../edit-processor.component.spec.ts               |  15 ++
 .../edit-processor/edit-processor.component.ts     |   8 +-
 .../edit-controller-service.component.spec.ts      |  23 +++
 .../edit-controller-service.component.ts           |  10 +-
 .../property-table/property-table.component.ts     |  12 +-
 .../app/ui/common/utils/parameter.utils.spec.ts    | 110 ++++++++++++
 .../src/app/ui/common/utils/parameter.utils.ts     |  47 +++++
 13 files changed, 653 insertions(+), 33 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts
index 747ce4e1d6c..99a2cbe0614 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts
@@ -26,6 +26,7 @@ import * as ControllerServicesActions from 
'./controller-services.actions';
 import {
     clearControllerServiceBulletins,
     clearControllerServiceBulletinsSuccess,
+    configureControllerServiceSuccess,
     createControllerService,
     createControllerServiceSuccess,
     deleteControllerService,
@@ -41,9 +42,10 @@ import { PropertyTableHelperService } from 
'../../../../service/property-table-h
 import { ParameterHelperService } from 
'../../service/parameter-helper.service';
 import { ExtensionTypesService } from 
'../../../../service/extension-types.service';
 import { Client } from '../../../../service/client.service';
-import { ComponentType, Storage } from '@nifi/shared';
+import { ComponentType, Storage, YesNoDialog } from '@nifi/shared';
+import { EventEmitter } from '@angular/core';
 import { ParameterContextService } from 
'../../../parameter-contexts/service/parameter-contexts.service';
-import { controllerServicesFeatureKey } from './index';
+import { ConfigureControllerServiceSuccess, controllerServicesFeatureKey } 
from './index';
 import { initialState } from './controller-services.reducer';
 import { ClearBulletinsRequest, ClearBulletinsResponse } from 
'../../../../state/shared';
 import * as ErrorActions from '../../../../state/error/error.actions';
@@ -642,4 +644,182 @@ describe('ControllerServicesEffects', () => {
             );
         });
     });
+
+    describe('openConfigureControllerServiceDialog$ goToParameter', () => {
+        const PARAMETER_CONTEXT_ID = 'ctx-1';
+        const SERVICE_ID = 'cs-1';
+        const EXPECTED_COMMANDS = ['/parameter-contexts', 
PARAMETER_CONTEXT_ID, 'edit'];
+        const EXPECTED_BACK_NAVIGATION = {
+            route: ['/process-groups', 'root', 'controller-services', 
SERVICE_ID, 'edit'],
+            routeBoundary: ['/parameter-contexts'],
+            context: 'Controller Service'
+        };
+
+        let editServiceInstance: any;
+        let saveChangesInstance: { yes: EventEmitter<any>; no: 
EventEmitter<any> };
+
+        // Opens the edit dialog and returns the goToParameter callback the 
effect assigned to it.
+        async function openDialogAndGetGoToParameter(dirty: boolean) {
+            const { effects } = await setup({
+                controllerServicesState: {
+                    ...initialState,
+                    parameterContext: {
+                        id: PARAMETER_CONTEXT_ID,
+                        permissions: { canRead: true, canWrite: true }
+                    }
+                }
+            });
+
+            const router = TestBed.inject(Router);
+
+            editServiceInstance = {
+                verify: new EventEmitter<any>(),
+                editControllerService: new EventEmitter<any>(),
+                editControllerServiceForm: { dirty },
+                submitForm: vi.fn()
+            };
+            saveChangesInstance = { yes: new EventEmitter<any>(), no: new 
EventEmitter<any>() };
+
+            const dialog = TestBed.inject(MatDialog);
+            vi.mocked(dialog.open).mockImplementation((component: any) => {
+                if (component === YesNoDialog) {
+                    return {
+                        componentInstance: saveChangesInstance,
+                        afterClosed: () => of()
+                    } as any;
+                }
+
+                return {
+                    close: vi.fn(),
+                    afterClosed: () => of(),
+                    componentInstance: editServiceInstance
+                } as any;
+            });
+
+            
vi.mocked(TestBed.inject(PropertyTableHelperService).getComponentHistory).mockReturnValue(
+                of({ componentHistory: {} }) as any
+            );
+            
vi.mocked(TestBed.inject(ParameterContextService).getParameterContext).mockReturnValue(
+                of({ id: PARAMETER_CONTEXT_ID, component: { name: 'ctx' } }) 
as any
+            );
+
+            effects.openConfigureControllerServiceDialog$.subscribe();
+            action$.next(
+                
ControllerServicesActions.openConfigureControllerServiceDialog({
+                    request: {
+                        id: SERVICE_ID,
+                        controllerService: { id: SERVICE_ID, uri: 
`https://localhost:4200/${SERVICE_ID}` }
+                    } as any
+                })
+            );
+
+            return { goToParameter: editServiceInstance.goToParameter, router 
};
+        }
+
+        it('navigates to the parameter context highlighting the referenced 
parameter when the form is pristine', async () => {
+            const { goToParameter, router } = await 
openDialogAndGetGoToParameter(false);
+
+            goToParameter('#{my-param}');
+
+            expect(router.navigate).toHaveBeenCalledWith(EXPECTED_COMMANDS, {
+                state: {
+                    backNavigation: EXPECTED_BACK_NAVIGATION,
+                    highlightedParameterName: 'my-param'
+                }
+            });
+        });
+
+        it('navigates without a highlight when the property value contains no 
parameter reference', async () => {
+            const { goToParameter, router } = await 
openDialogAndGetGoToParameter(false);
+
+            goToParameter('a literal value');
+
+            const state = vi.mocked(router.navigate).mock.calls[0][1]?.state 
?? {};
+            expect(Object.keys(state)).toEqual(['backNavigation']);
+        });
+
+        it('submits the form with the highlight when the form is dirty and 
changes are saved', async () => {
+            const { goToParameter, router } = await 
openDialogAndGetGoToParameter(true);
+
+            goToParameter('#{my-param}');
+            saveChangesInstance.yes.emit();
+
+            
expect(editServiceInstance.submitForm).toHaveBeenCalledWith(EXPECTED_COMMANDS, 
['/parameter-contexts'], {
+                highlightedParameterName: 'my-param'
+            });
+            expect(router.navigate).not.toHaveBeenCalled();
+        });
+
+        it('navigates with the highlight when the form is dirty and changes 
are discarded', async () => {
+            const { goToParameter, router } = await 
openDialogAndGetGoToParameter(true);
+
+            goToParameter('#{my-param}');
+            saveChangesInstance.no.emit();
+
+            expect(editServiceInstance.submitForm).not.toHaveBeenCalled();
+            expect(router.navigate).toHaveBeenCalledWith(EXPECTED_COMMANDS, {
+                state: {
+                    backNavigation: EXPECTED_BACK_NAVIGATION,
+                    highlightedParameterName: 'my-param'
+                }
+            });
+        });
+    });
+
+    describe('configureControllerServiceSuccess$', () => {
+        it('should include postUpdateNavigationState in router navigate state 
when postUpdateNavigationState is provided', async () => {
+            const { effects } = await setup();
+            const router = TestBed.inject(Router);
+
+            const response: ConfigureControllerServiceSuccess = {
+                id: 'cs-1',
+                controllerService: {} as any,
+                postUpdateNavigation: ['/parameter-contexts', 'ctx-1', 'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'my-param' }
+            };
+
+            effects.configureControllerServiceSuccess$.subscribe();
+            action$.next(configureControllerServiceSuccess({ response }));
+
+            
expect(router.navigate).toHaveBeenCalledWith(['/parameter-contexts', 'ctx-1', 
'edit'], {
+                state: {
+                    backNavigation: {
+                        route: ['/process-groups', 'root', 
'controller-services', 'cs-1', 'edit'],
+                        routeBoundary: ['/parameter-contexts'],
+                        context: 'Controller Service'
+                    },
+                    highlightedParameterName: 'my-param'
+                }
+            });
+        });
+
+        it('should omit postUpdateNavigationState keys from router navigate 
state when postUpdateNavigationState is absent', async () => {
+            const { effects } = await setup();
+            const router = TestBed.inject(Router);
+
+            const response: ConfigureControllerServiceSuccess = {
+                id: 'cs-2',
+                controllerService: {} as any,
+                postUpdateNavigation: ['/parameter-contexts', 'ctx-2', 'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts']
+            };
+
+            effects.configureControllerServiceSuccess$.subscribe();
+            action$.next(configureControllerServiceSuccess({ response }));
+
+            expect(router.navigate).toHaveBeenCalledTimes(1);
+            
expect(vi.mocked(router.navigate).mock.calls[0][0]).toEqual(['/parameter-contexts',
 'ctx-2', 'edit']);
+
+            // Object.keys rather than toHaveBeenCalledWith: argument matching 
treats an absent key and an
+            // explicit undefined as equal, so it cannot distinguish omission 
from highlightedParameterName: undefined.
+            const state = vi.mocked(router.navigate).mock.calls[0][1]?.state 
?? {};
+            expect(Object.keys(state)).toEqual(['backNavigation']);
+            expect(state['backNavigation']).toEqual({
+                route: ['/process-groups', 'root', 'controller-services', 
'cs-2', 'edit'],
+                routeBoundary: ['/parameter-contexts'],
+                context: 'Controller Service'
+            });
+        });
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts
index f0fc80574cb..ad185a28342 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts
@@ -35,9 +35,11 @@ import {
     ControllerServiceReferencingComponent,
     EditControllerServiceDialogRequest,
     OpenChangeComponentVersionDialogRequest,
+    PostUpdateNavigationState,
     UpdateControllerServiceRequest
 } from '../../../../state/shared';
 import { Router } from '@angular/router';
+import { extractParameterName } from 
'../../../../ui/common/utils/parameter.utils';
 import {
     selectCurrentProcessGroupId,
     selectLoadedTimestamp,
@@ -406,7 +408,12 @@ export class ControllerServicesEffects {
                         selectPropertyVerificationStatus
                     );
 
-                    const goTo = (commands: string[], destination: string, 
commandBoundary?: string[]): void => {
+                    const goTo = (
+                        commands: string[],
+                        destination: string,
+                        commandBoundary?: string[],
+                        navigationState?: PostUpdateNavigationState
+                    ): void => {
                         if 
(editDialogReference.componentInstance.editControllerServiceForm.dirty) {
                             const saveChangesDialogReference = 
this.dialog.open(YesNoDialog, {
                                 ...SMALL_DIALOG,
@@ -417,7 +424,11 @@ export class ControllerServicesEffects {
                             });
 
                             
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
-                                
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
+                                
editDialogReference.componentInstance.submitForm(
+                                    commands,
+                                    commandBoundary,
+                                    navigationState
+                                );
                             });
 
                             
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
@@ -434,7 +445,8 @@ export class ControllerServicesEffects {
                                                 ],
                                                 routeBoundary: commandBoundary,
                                                 context: 'Controller Service'
-                                            } as BackNavigation
+                                            } as BackNavigation,
+                                            ...navigationState
                                         }
                                     });
                                 } else {
@@ -455,7 +467,8 @@ export class ControllerServicesEffects {
                                             ],
                                             routeBoundary: commandBoundary,
                                             context: 'Controller Service'
-                                        } as BackNavigation
+                                        } as BackNavigation,
+                                        ...navigationState
                                     }
                                 });
                             } else {
@@ -473,12 +486,18 @@ export class ControllerServicesEffects {
 
                     if (parameterContext != null) {
                         editDialogReference.componentInstance.parameterContext 
= parameterContext;
-                        editDialogReference.componentInstance.goToParameter = 
() => {
+                        editDialogReference.componentInstance.goToParameter = 
(parameterValue: string) => {
                             
this.storage.setItem<number>(NiFiCommon.EDIT_PARAMETER_CONTEXT_DIALOG_ID, 1);
 
+                            const parameterName = 
extractParameterName(parameterValue);
                             const commandBoundary: string[] = 
['/parameter-contexts'];
                             const commands: string[] = [...commandBoundary, 
parameterContext.id, 'edit'];
-                            goTo(commands, 'Parameter', commandBoundary);
+                            goTo(
+                                commands,
+                                'Parameter',
+                                commandBoundary,
+                                parameterName ? { highlightedParameterName: 
parameterName } : undefined
+                            );
                         };
 
                         
editDialogReference.componentInstance.convertToParameter =
@@ -533,7 +552,9 @@ export class ControllerServicesEffects {
                                         payload: 
updateControllerServiceRequest.payload,
                                         postUpdateNavigation: 
updateControllerServiceRequest.postUpdateNavigation,
                                         postUpdateNavigationBoundary:
-                                            
updateControllerServiceRequest.postUpdateNavigationBoundary
+                                            
updateControllerServiceRequest.postUpdateNavigationBoundary,
+                                        postUpdateNavigationState:
+                                            
updateControllerServiceRequest.postUpdateNavigationState
                                     }
                                 })
                             );
@@ -570,7 +591,8 @@ export class ControllerServicesEffects {
                                 id: request.id,
                                 controllerService: response,
                                 postUpdateNavigation: 
request.postUpdateNavigation,
-                                postUpdateNavigationBoundary: 
request.postUpdateNavigationBoundary
+                                postUpdateNavigationBoundary: 
request.postUpdateNavigationBoundary,
+                                postUpdateNavigationState: 
request.postUpdateNavigationState
                             }
                         })
                     ),
@@ -625,7 +647,8 @@ export class ControllerServicesEffects {
                                         ],
                                         routeBoundary: 
response.postUpdateNavigationBoundary,
                                         context: 'Controller Service'
-                                    } as BackNavigation
+                                    } as BackNavigation,
+                                    ...response.postUpdateNavigationState
                                 }
                             });
                         } else {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/index.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/index.ts
index 940ecc4834c..c5cfb5c238d 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/index.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/index.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { BreadcrumbEntity, ControllerServiceEntity } from 
'../../../../state/shared';
+import { BreadcrumbEntity, ControllerServiceEntity, PostUpdateNavigationState 
} from '../../../../state/shared';
 import { ParameterContextReferenceEntity } from '@nifi/shared';
 
 export const controllerServicesFeatureKey = 'controllerServiceListing';
@@ -42,6 +42,7 @@ export interface ConfigureControllerServiceRequest {
     payload: any;
     postUpdateNavigation?: string[];
     postUpdateNavigationBoundary?: string[];
+    postUpdateNavigationState?: PostUpdateNavigationState;
 }
 
 export interface ConfigureControllerServiceSuccess {
@@ -49,6 +50,7 @@ export interface ConfigureControllerServiceSuccess {
     controllerService: ControllerServiceEntity;
     postUpdateNavigation?: string[];
     postUpdateNavigationBoundary?: string[];
+    postUpdateNavigationState?: PostUpdateNavigationState;
 }
 
 export interface DeleteControllerServiceRequest {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
index e2f21e170fe..f8ffd720ac8 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
@@ -43,7 +43,8 @@ import {
     EnableComponentRequest,
     StartComponentRequest,
     StopComponentRequest,
-    UpdateProcessorRequest
+    UpdateProcessorRequest,
+    UpdateProcessorResponse
 } from '../../../../state/shared';
 import { selectCurrentUser } from 
'../../../../state/current-user/current-user.selectors';
 import * as fromUser from 
'../../../../state/current-user/current-user.reducer';
@@ -63,7 +64,7 @@ import { CopyPasteService } from 
'../../service/copy-paste.service';
 import { CanvasView } from '../../service/canvas-view.service';
 import { BirdseyeView } from '../../service/birdseye-view.service';
 import { selectDisconnectionAcknowledged } from 
'../../../../state/cluster-summary/cluster-summary.selectors';
-import { ComponentType, ComponentTypeNamePipe } from '@nifi/shared';
+import { ComponentType, ComponentTypeNamePipe, YesNoDialog } from 
'@nifi/shared';
 import { ParameterContextService } from 
'../../../parameter-contexts/service/parameter-contexts.service';
 import { HttpErrorResponse } from '@angular/common/http';
 import { provideRouter, Router } from '@angular/router';
@@ -853,7 +854,8 @@ describe('FlowEffects', () => {
                 {
                     provide: ParameterHelperService,
                     useValue: {
-                        getParameterContext: vi.fn()
+                        getParameterContext: vi.fn(),
+                        convertToParameter: vi.fn()
                     }
                 },
                 {
@@ -1546,4 +1548,190 @@ describe('FlowEffects', () => {
             );
         });
     });
+
+    describe('openEditProcessorDialog$ goToParameter', () => {
+        const PARAMETER_CONTEXT_ID = 'ctx-1';
+        const PROCESSOR_ID = 'd90ac264-018b-1000-1827-a86c8156fd9e';
+        const EXPECTED_COMMANDS = ['/parameter-contexts', 
PARAMETER_CONTEXT_ID, 'edit'];
+        const EXPECTED_BACK_NAVIGATION = {
+            route: ['/process-groups', 'pg-123', ComponentType.Processor, 
PROCESSOR_ID, 'edit'],
+            routeBoundary: ['/parameter-contexts'],
+            context: 'Processor'
+        };
+
+        let router: Router;
+        let editProcessorInstance: any;
+        let saveChangesInstance: { yes: EventEmitter<any>; no: 
EventEmitter<any> };
+
+        // Opens the edit dialog and returns the goToParameter callback the 
effect assigned to it.
+        const openDialogAndGetGoToParameter = (dirty: boolean): 
((parameterValue: string) => void) => {
+            editProcessorInstance = {
+                ...MockComponent(EditProcessor),
+                verify,
+                editProcessor,
+                startComponentRequest: startRequest,
+                stopComponentRequest: stopRequest,
+                disableComponentRequest: disableRequest,
+                enableComponentRequest: enableRequest,
+                editProcessorForm: { dirty },
+                submitForm: vi.fn()
+            };
+            saveChangesInstance = { yes: new EventEmitter<any>(), no: new 
EventEmitter<any>() };
+
+            vi.spyOn(dialog, 'open').mockImplementation((component: any) => {
+                if (component === YesNoDialog) {
+                    return {
+                        componentInstance: saveChangesInstance,
+                        afterClosed: () => of()
+                    } as unknown as MatDialogRef<any>;
+                }
+
+                return {
+                    close: vi.fn(),
+                    afterClosed: () => of(),
+                    componentInstance: editProcessorInstance
+                } as unknown as MatDialogRef<EditProcessor>;
+            });
+
+            effects.openEditProcessorDialog$.subscribe();
+            action$.next(
+                FlowActions.openEditProcessorDialog({
+                    request: {
+                        type: mockData.type,
+                        uri: mockData.uri,
+                        entity: mockData.entity
+                    } as any
+                })
+            );
+
+            return editProcessorInstance.goToParameter;
+        };
+
+        beforeEach(() => {
+            router = TestBed.inject(Router);
+            vi.spyOn(router, 'navigate').mockImplementation(() => 
Promise.resolve(true));
+
+            store.overrideSelector(selectCurrentProcessGroupId, 'pg-123');
+            
store.overrideSelector(flowSelectors.selectCurrentParameterContext, {
+                id: PARAMETER_CONTEXT_ID,
+                permissions: { canRead: true, canWrite: true }
+            } as any);
+            store.refreshState();
+
+            vi.spyOn(TestBed.inject(ParameterContextService), 
'getParameterContext').mockReturnValue(
+                of({ id: PARAMETER_CONTEXT_ID, component: { name: 'ctx' } }) 
as any
+            );
+        });
+
+        it('navigates to the parameter context highlighting the referenced 
parameter when the form is pristine', () => {
+            const goToParameter = openDialogAndGetGoToParameter(false);
+
+            goToParameter('#{my-param}');
+
+            expect(router.navigate).toHaveBeenCalledWith(EXPECTED_COMMANDS, {
+                state: {
+                    backNavigation: EXPECTED_BACK_NAVIGATION,
+                    highlightedParameterName: 'my-param'
+                }
+            });
+        });
+
+        it('navigates without a highlight when the property value contains no 
parameter reference', () => {
+            const goToParameter = openDialogAndGetGoToParameter(false);
+
+            goToParameter('a literal value');
+
+            const state = vi.mocked(router.navigate).mock.calls[0][1]?.state 
?? {};
+            expect(Object.keys(state)).toEqual(['backNavigation']);
+        });
+
+        it('submits the form with the highlight when the form is dirty and 
changes are saved', () => {
+            const goToParameter = openDialogAndGetGoToParameter(true);
+
+            goToParameter('#{my-param}');
+            saveChangesInstance.yes.emit();
+
+            
expect(editProcessorInstance.submitForm).toHaveBeenCalledWith(EXPECTED_COMMANDS,
 ['/parameter-contexts'], {
+                highlightedParameterName: 'my-param'
+            });
+            expect(router.navigate).not.toHaveBeenCalled();
+        });
+
+        it('navigates with the highlight when the form is dirty and changes 
are discarded', () => {
+            const goToParameter = openDialogAndGetGoToParameter(true);
+
+            goToParameter('#{my-param}');
+            saveChangesInstance.no.emit();
+
+            expect(editProcessorInstance.submitForm).not.toHaveBeenCalled();
+            expect(router.navigate).toHaveBeenCalledWith(EXPECTED_COMMANDS, {
+                state: {
+                    backNavigation: EXPECTED_BACK_NAVIGATION,
+                    highlightedParameterName: 'my-param'
+                }
+            });
+        });
+    });
+
+    describe('updateProcessorSuccess$', () => {
+        let router: Router;
+
+        beforeEach(() => {
+            router = TestBed.inject(Router);
+            vi.spyOn(router, 'navigate').mockImplementation(() => 
Promise.resolve(true));
+            store.overrideSelector(selectCurrentProcessGroupId, 'pg-123');
+            store.refreshState();
+        });
+
+        it('should include postUpdateNavigationState in router navigate state 
when postUpdateNavigationState is provided', () => {
+            const response: UpdateProcessorResponse = {
+                id: 'proc-1',
+                type: ComponentType.Processor,
+                postUpdateNavigation: ['/parameter-contexts', 'ctx-1', 'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'my-param' },
+                response: {}
+            };
+
+            effects.updateProcessorSuccess$.subscribe();
+            action$.next(FlowActions.updateProcessorSuccess({ response }));
+
+            
expect(router.navigate).toHaveBeenCalledWith(['/parameter-contexts', 'ctx-1', 
'edit'], {
+                state: {
+                    backNavigation: {
+                        route: ['/process-groups', 'pg-123', 
ComponentType.Processor, 'proc-1', 'edit'],
+                        routeBoundary: ['/parameter-contexts'],
+                        context: 'Processor'
+                    },
+                    highlightedParameterName: 'my-param'
+                }
+            });
+        });
+
+        it('should omit postUpdateNavigationState keys from router navigate 
state when postUpdateNavigationState is absent', () => {
+            const response: UpdateProcessorResponse = {
+                id: 'proc-2',
+                type: ComponentType.Processor,
+                postUpdateNavigation: ['/parameter-contexts', 'ctx-2', 'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                response: {}
+            };
+
+            effects.updateProcessorSuccess$.subscribe();
+            action$.next(FlowActions.updateProcessorSuccess({ response }));
+
+            expect(router.navigate).toHaveBeenCalledTimes(1);
+            
expect(vi.mocked(router.navigate).mock.calls[0][0]).toEqual(['/parameter-contexts',
 'ctx-2', 'edit']);
+
+            // Object.keys rather than toHaveBeenCalledWith: argument matching 
treats an absent key and an
+            // explicit undefined as equal, so it cannot distinguish omission 
from highlightedParameterName: undefined.
+            const state = vi.mocked(router.navigate).mock.calls[0][1]?.state 
?? {};
+            expect(Object.keys(state)).toEqual(['backNavigation']);
+            expect(state['backNavigation']).toEqual({
+                route: ['/process-groups', 'pg-123', ComponentType.Processor, 
'proc-2', 'edit'],
+                routeBoundary: ['/parameter-contexts'],
+                context: 'Processor'
+            });
+        });
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index a66564ab5bc..5413002cecf 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -106,6 +106,7 @@ import {
     EnableComponentRequest,
     OpenChangeComponentVersionDialogRequest,
     ParameterContextEntity,
+    PostUpdateNavigationState,
     RegistryClientEntity,
     StartComponentRequest,
     StopComponentRequest,
@@ -180,6 +181,7 @@ import {
 } from 
'../../../../state/property-verification/property-verification.selectors';
 import { VerifyPropertiesRequestContext } from 
'../../../../state/property-verification';
 import { BackNavigation } from '../../../../state/navigation';
+import { extractParameterName } from 
'../../../../ui/common/utils/parameter.utils';
 import { resetPollingFlowAnalysis } from 
'../flow-analysis/flow-analysis.actions';
 import { selectDocumentVisibilityState } from 
'../../../../state/document-visibility/document-visibility.selectors';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -1564,7 +1566,12 @@ export class FlowEffects {
                         selectPropertyVerificationStatus
                     );
 
-                    const goTo = (commands: string[], commandBoundary: 
string[], destination: string): void => {
+                    const goTo = (
+                        commands: string[],
+                        commandBoundary: string[],
+                        destination: string,
+                        navigationState?: PostUpdateNavigationState
+                    ): void => {
                         if 
(editDialogReference.componentInstance.editProcessorForm.dirty) {
                             const saveChangesDialogReference = 
this.dialog.open(YesNoDialog, {
                                 ...SMALL_DIALOG,
@@ -1575,7 +1582,11 @@ export class FlowEffects {
                             });
 
                             
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
-                                
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
+                                
editDialogReference.componentInstance.submitForm(
+                                    commands,
+                                    commandBoundary,
+                                    navigationState
+                                );
                             });
 
                             
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
@@ -1591,7 +1602,8 @@ export class FlowEffects {
                                             ],
                                             routeBoundary: commandBoundary,
                                             context: 'Processor'
-                                        } as BackNavigation
+                                        } as BackNavigation,
+                                        ...navigationState
                                     }
                                 });
                             });
@@ -1608,7 +1620,8 @@ export class FlowEffects {
                                         ],
                                         routeBoundary: commandBoundary,
                                         context: 'Processor'
-                                    } as BackNavigation
+                                    } as BackNavigation,
+                                    ...navigationState
                                 }
                             });
                         }
@@ -1616,12 +1629,18 @@ export class FlowEffects {
 
                     if (parameterContext != null) {
                         editDialogReference.componentInstance.parameterContext 
= parameterContext;
-                        editDialogReference.componentInstance.goToParameter = 
() => {
+                        editDialogReference.componentInstance.goToParameter = 
(parameterValue: string) => {
                             
this.storage.setItem<number>(NiFiCommon.EDIT_PARAMETER_CONTEXT_DIALOG_ID, 1);
 
+                            const parameterName = 
extractParameterName(parameterValue);
                             const commandBoundary: string[] = 
['/parameter-contexts'];
                             const commands: string[] = [...commandBoundary, 
parameterContext.id, 'edit'];
-                            goTo(commands, commandBoundary, 'Parameter');
+                            goTo(
+                                commands,
+                                commandBoundary,
+                                'Parameter',
+                                parameterName ? { highlightedParameterName: 
parameterName } : undefined
+                            );
                         };
 
                         
editDialogReference.componentInstance.convertToParameter =
@@ -2231,6 +2250,7 @@ export class FlowEffects {
                             type: request.type,
                             postUpdateNavigation: request.postUpdateNavigation,
                             postUpdateNavigationBoundary: 
request.postUpdateNavigationBoundary,
+                            postUpdateNavigationState: 
request.postUpdateNavigationState,
                             response
                         };
                         return FlowActions.updateProcessorSuccess({ response: 
updateProcessorResponse });
@@ -2270,7 +2290,8 @@ export class FlowEffects {
                                     ],
                                     routeBoundary: 
response.postUpdateNavigationBoundary,
                                     context: 'Processor'
-                                } as BackNavigation
+                                } as BackNavigation,
+                                ...response.postUpdateNavigationState
                             }
                         });
                     } else {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/state/shared/index.ts 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/state/shared/index.ts
index e1fafc075bf..144bbec4ca7 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/state/shared/index.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/state/shared/index.ts
@@ -216,11 +216,13 @@ export interface UpdateComponentFailure {
 export interface UpdateProcessorRequest extends UpdateComponentRequest {
     postUpdateNavigation?: string[];
     postUpdateNavigationBoundary?: string[];
+    postUpdateNavigationState?: PostUpdateNavigationState;
 }
 
 export interface UpdateProcessorResponse extends UpdateComponentResponse {
     postUpdateNavigation?: string[];
     postUpdateNavigationBoundary?: string[];
+    postUpdateNavigationState?: PostUpdateNavigationState;
 }
 
 export interface UpdateConnectionRequest extends UpdateComponentRequest {
@@ -330,6 +332,7 @@ export interface UpdateControllerServiceRequest {
     payload: any;
     postUpdateNavigation?: string[];
     postUpdateNavigationBoundary?: string[];
+    postUpdateNavigationState?: PostUpdateNavigationState;
 }
 
 export interface SetEnableControllerServiceDialogRequest {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.spec.ts
index f0a7c261c88..1a68a2546d1 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.spec.ts
@@ -763,6 +763,21 @@ describe('EditProcessor', () => {
             'The number of tasks that should be concurrently scheduled for 
this processor. Must be an integer greater than 0.'
         );
     });
+
+    it('should forward postUpdateNavigationState from submitForm to the edit 
action', () => {
+        vi.spyOn(component.editProcessor, 'next');
+        component.submitForm(['/parameter-contexts', 'ctx-1', 'edit'], 
['/parameter-contexts'], {
+            highlightedParameterName: 'my-param'
+        });
+
+        expect(component.editProcessor.next).toHaveBeenCalledWith(
+            expect.objectContaining({
+                postUpdateNavigation: ['/parameter-contexts', 'ctx-1', 'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'my-param' }
+            })
+        );
+    });
 });
 
 describe('EditProcessor with TriggerSerially', () => {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.ts
index 1a8e832521d..5870d6bac92 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-dialogs/edit-processor/edit-processor.component.ts
@@ -45,6 +45,7 @@ import {
     InlineServiceCreationRequest,
     InlineServiceCreationResponse,
     ParameterContextEntity,
+    PostUpdateNavigationState,
     Property,
     StartComponentRequest,
     StopComponentRequest,
@@ -416,7 +417,11 @@ export class EditProcessor extends TabbedDialog {
         );
     }
 
-    submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: 
string[]) {
+    submitForm(
+        postUpdateNavigation?: string[],
+        postUpdateNavigationBoundary?: string[],
+        postUpdateNavigationState?: PostUpdateNavigationState
+    ) {
         const relationshipConfiguration: RelationshipConfiguration =
             this.editProcessorForm.get('relationshipConfiguration')?.value;
         const autoTerminated: string[] = 
relationshipConfiguration.relationships
@@ -481,6 +486,7 @@ export class EditProcessor extends TabbedDialog {
             errorStrategy: 'banner',
             postUpdateNavigation,
             postUpdateNavigationBoundary,
+            postUpdateNavigationState,
             payload
         });
     }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.spec.ts
index 87b19e3a094..20f08fc71bd 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.spec.ts
@@ -621,6 +621,29 @@ describe('EditControllerService', () => {
         });
     });
 
+    it('should forward postUpdateNavigationState from submitForm to the edit 
action', () => {
+        const mockFormData = {
+            name: 'service-name',
+            bulletinLevel: 'DEBUG',
+            comments: 'service-comments',
+            properties: []
+        };
+
+        vi.spyOn(component.editControllerService, 'next');
+        component.editControllerServiceForm.setValue(mockFormData);
+        component.submitForm(['/parameter-contexts', 'ctx-1', 'edit'], 
['/parameter-contexts'], {
+            highlightedParameterName: 'my-param'
+        });
+
+        expect(component.editControllerService.next).toHaveBeenCalledWith(
+            expect.objectContaining({
+                postUpdateNavigation: ['/parameter-contexts', 'ctx-1', 'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'my-param' }
+            })
+        );
+    });
+
     describe('readonly derivation', () => {
         function buildRequest(overrides: {
             readonly?: boolean;
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts
index 643331b2417..490b11db023 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts
@@ -27,6 +27,7 @@ import {
     InlineServiceCreationRequest,
     InlineServiceCreationResponse,
     ParameterContextEntity,
+    PostUpdateNavigationState,
     Property,
     UpdateControllerServiceRequest
 } from '../../../../state/shared';
@@ -179,7 +180,11 @@ export class EditControllerService extends TabbedDialog {
         return this.nifiCommon.formatBundle(entity.component.bundle);
     }
 
-    submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: 
string[]) {
+    submitForm(
+        postUpdateNavigation?: string[],
+        postUpdateNavigationBoundary?: string[],
+        postUpdateNavigationState?: PostUpdateNavigationState
+    ) {
         const payload: any = {
             revision: this.client.getRevision(this.request.controllerService),
             disconnectedNodeAcknowledged: 
this.clusterConnectionService.isDisconnectionAcknowledged(),
@@ -203,7 +208,8 @@ export class EditControllerService extends TabbedDialog {
         this.editControllerService.next({
             payload,
             postUpdateNavigation,
-            postUpdateNavigationBoundary
+            postUpdateNavigationBoundary,
+            postUpdateNavigationState
         });
     }
 
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/property-table.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/property-table.component.ts
index 4abc9dcc831..bb5f03c3950 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/property-table.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/property-table.component.ts
@@ -62,6 +62,7 @@ import { takeUntilDestroyed } from 
'@angular/core/rxjs-interop';
 import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
 import { PropertyItem } from './property-item';
 import { PropertyValueTip } from 
'../tooltips/property-value-tip/property-value-tip.component';
+import { PARAM_REF_REGEX } from '../utils/parameter.utils';
 
 @Component({
     selector: 'property-table',
@@ -107,8 +108,6 @@ export class PropertyTable implements AfterViewInit, 
ControlValueAccessor {
     @Input() propertyHistory: ComponentHistory | undefined;
     @Input() supportsParameters = true;
 
-    private static readonly PARAM_REF_REGEX: RegExp = /#{(['"]?)[a-zA-Z0-9-_. 
]+\1}/;
-
     private destroyRef = inject(DestroyRef);
 
     protected readonly NfEditor = NfEditor;
@@ -229,7 +228,7 @@ export class PropertyTable implements AfterViewInit, 
ControlValueAccessor {
         let dependentValue = dependentItem.value;
         if (dependentValue != null) {
             // check if the dependent value is a parameter reference
-            if (PropertyTable.PARAM_REF_REGEX.test(dependentValue)) {
+            if (PARAM_REF_REGEX.test(dependentValue)) {
                 // the dependent value contains parameter reference, if the 
user can view
                 // the parameter context resolve the parameter value to see if 
it
                 // satisfies the dependent values
@@ -517,11 +516,8 @@ export class PropertyTable implements AfterViewInit, 
ControlValueAccessor {
     }
 
     canGoToParameter(item: PropertyItem): boolean {
-        // TODO - currently parameter context route does not support navigating
-        // directly to a specific parameter so the parameter context link
-        // is not item specific.
         if (this.parameterContext && this.goToParameter && item.value) {
-            return this.parameterContext.permissions.canRead && 
PropertyTable.PARAM_REF_REGEX.test(item.value);
+            return this.parameterContext.permissions.canRead && 
PARAM_REF_REGEX.test(item.value);
         }
 
         return false;
@@ -543,7 +539,7 @@ export class PropertyTable implements AfterViewInit, 
ControlValueAccessor {
 
         let propertyReferencesParameter = false;
         if (canUpdateParameterContext && item.value) {
-            propertyReferencesParameter = 
PropertyTable.PARAM_REF_REGEX.test(item.value);
+            propertyReferencesParameter = PARAM_REF_REGEX.test(item.value);
         }
 
         return canUpdateParameterContext && !propertyReferencesParameter;
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/utils/parameter.utils.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/utils/parameter.utils.spec.ts
new file mode 100644
index 00000000000..0196acfe5c1
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/utils/parameter.utils.spec.ts
@@ -0,0 +1,110 @@
+/*
+ * 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 { extractParameterName, PARAM_REF_REGEX } from './parameter.utils';
+
+describe('extractParameterName', () => {
+    describe('valid unquoted references', () => {
+        it('extracts a simple name', () => {
+            expect(extractParameterName('#{my-param}')).toBe('my-param');
+        });
+
+        it('extracts a name with underscores and dots', () => {
+            expect(extractParameterName('#{my_param.v2}')).toBe('my_param.v2');
+        });
+
+        it('extracts a name with spaces', () => {
+            expect(extractParameterName('#{param with spaces}')).toBe('param 
with spaces');
+        });
+
+        it('trims surrounding whitespace inside an unquoted reference', () => {
+            expect(extractParameterName('#{ my-param }')).toBe('my-param');
+        });
+
+        it('extracts a single-character name', () => {
+            expect(extractParameterName('#{x}')).toBe('x');
+        });
+    });
+
+    describe('valid single-quoted references', () => {
+        it('extracts a name wrapped in single quotes', () => {
+            expect(extractParameterName("#{'my-param'}")).toBe('my-param');
+        });
+
+        it('extracts a name with spaces wrapped in single quotes', () => {
+            expect(extractParameterName("#{'param with spaces'}")).toBe('param 
with spaces');
+        });
+    });
+
+    describe('valid double-quoted references', () => {
+        it('extracts a name wrapped in double quotes', () => {
+            expect(extractParameterName('#{"my-param"}')).toBe('my-param');
+        });
+
+        it('extracts a name with spaces wrapped in double quotes', () => {
+            expect(extractParameterName('#{"param with spaces"}')).toBe('param 
with spaces');
+        });
+    });
+
+    describe('non-matching values', () => {
+        it('returns undefined for a plain string', () => {
+            expect(extractParameterName('plain-value')).toBeUndefined();
+        });
+
+        it('returns undefined for an empty string', () => {
+            expect(extractParameterName('')).toBeUndefined();
+        });
+
+        it('returns undefined for #{} with no name', () => {
+            expect(extractParameterName('#{}')).toBeUndefined();
+        });
+
+        it('returns undefined for #{ } with only whitespace', () => {
+            expect(extractParameterName('#{ }')).toBeUndefined();
+        });
+
+        it('returns undefined when quotes are mismatched', () => {
+            expect(extractParameterName('#{\'mismatched"}')).toBeUndefined();
+        });
+    });
+
+    describe('references embedded in surrounding content', () => {
+        it('returns the parameter name when the reference has trailing 
content', () => {
+            expect(extractParameterName('#{param} extra')).toBe('param');
+        });
+
+        it('returns the parameter name when the reference has leading 
content', () => {
+            expect(extractParameterName('prefix #{param}')).toBe('param');
+        });
+
+        it('returns the first parameter name when the value contains multiple 
references', () => {
+            expect(extractParameterName('#{p1}-#{p2}')).toBe('p1');
+        });
+
+        it('skips an invalid reference and extracts the first valid one', () 
=> {
+            expect(extractParameterName('#{bad:name} 
#{kafka.brokers}')).toBe('kafka.brokers');
+        });
+    });
+});
+
+describe('PARAM_REF_REGEX', () => {
+    it('matches the same first valid reference that extractParameterName 
returns', () => {
+        const value = '#{bad:name} #{kafka.brokers}';
+        expect(PARAM_REF_REGEX.test(value)).toBe(true);
+        expect(extractParameterName(value)).toBe('kafka.brokers');
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/utils/parameter.utils.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/utils/parameter.utils.ts
new file mode 100644
index 00000000000..78adba137bb
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/utils/parameter.utils.ts
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+/**
+ * Matches a NiFi parameter reference anywhere in a property value.
+ *
+ * Supported forms:
+ *   #{name}
+ *   #{'name'}
+ *   #{"name"}
+ *
+ * The name charset matches the historical Property Table gate: letters, 
digits,
+ * hyphen, underscore, dot, and space. Group 2 captures the name.
+ */
+export const PARAM_REF_REGEX = /#{(['"]?)([a-zA-Z0-9-_. ]+)\1}/;
+
+/**
+ * Extracts the first parameter name from a NiFi parameter reference 
expression.
+ *
+ * Returns the trimmed name from the first matching `#{…}` reference in the 
value,
+ * or `undefined` when no reference is present or the captured name is empty
+ * after trimming (`#{}`, `#{ }`).
+ *
+ * When the value embeds a reference among other content (`prefix #{param} 
suffix`)
+ * or contains multiple references (`#{p1} and #{p2}`), the name from the first
+ * matching reference is returned. A future enhancement could surface a 
sub-menu
+ * for all referenced parameters (analogous to the `PropertyValueTip` tooltip),
+ * allowing the user to choose which one to navigate to.
+ */
+export function extractParameterName(value: string): string | undefined {
+    const match = PARAM_REF_REGEX.exec(value);
+    return match?.[2]?.trim() || undefined;
+}

Reply via email to