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 d0a06ddd87f NIFI-16217 - Fix: Unsaved Parameter Context changes 
silently lost when navigating to an inherited Parameter Context (#11559)
d0a06ddd87f is described below

commit d0a06ddd87f615dec76ae9c6f678016d8185431b
Author: Rob Fellows <[email protected]>
AuthorDate: Fri Aug 21 13:03:24 2026 -0400

    NIFI-16217 - Fix: Unsaved Parameter Context changes silently lost when 
navigating to an inherited Parameter Context (#11559)
    
    * NIFI-16217 - Fix: Unsaved Parameter Context changes silently lost when 
navigating to an inherited Parameter Context
    
    * address review feedback
---
 .../state/parameter-context-listing/index.ts       |  11 +-
 .../parameter-context-listing.actions.ts           |  10 +-
 .../parameter-context-listing.effects.spec.ts      | 401 ++++++++++++++++++++-
 .../parameter-context-listing.effects.ts           | 143 +++++++-
 .../parameter-context-listing.reducer.spec.ts      | 151 ++++++++
 .../parameter-context-listing.reducer.ts           |  17 +-
 .../parameter-context-listing.selectors.ts         |  20 +
 .../parameter-context-listing.component.spec.ts    | 114 +++++-
 .../parameter-context-listing.component.ts         |  22 +-
 .../parameter-table/parameter-table.component.html |   3 +-
 .../parameter-table.component.spec.ts              | 161 +++++++++
 .../parameter-table/parameter-table.component.ts   |  52 ++-
 .../apps/nifi/src/app/state/shared/index.ts        |   7 +
 .../edit-parameter-context.component.html          |  40 +-
 .../edit-parameter-context.component.spec.ts       | 143 +++++++-
 .../edit-parameter-context.component.ts            |  27 +-
 .../src/app/ui/common/parameter-context/index.ts   |   9 +-
 ...parameter-context-changes-dialog.component.html |  82 +++++
 ...ameter-context-changes-dialog.component.spec.ts | 110 ++++++
 ...e-parameter-context-changes-dialog.component.ts |  46 +++
 nifi-frontend/src/main/frontend/nx.json            |   3 +-
 21 files changed, 1504 insertions(+), 68 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts
index ec65dd7b25d..4a3761b0736 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts
@@ -15,7 +15,11 @@
  * limitations under the License.
  */
 
-import { ParameterContextEntity, ParameterContextUpdateRequestEntity } from 
'../../../../state/shared';
+import {
+    ParameterContextEntity,
+    ParameterContextUpdateRequestEntity,
+    PostUpdateNavigationState
+} from '../../../../state/shared';
 
 export const parameterContextListingFeatureKey = 'parameterContextListing';
 
@@ -34,10 +38,12 @@ export interface CreateParameterContextSuccess {
 
 export interface GetEffectiveParameterContext {
     id: string;
+    highlightedParameterName?: string;
 }
 
 export interface EditParameterContextRequest {
     parameterContext?: ParameterContextEntity;
+    highlightedParameterName?: string;
 }
 
 export interface DeleteParameterContextRequest {
@@ -56,6 +62,9 @@ export interface ParameterContextListingState {
     parameterContexts: ParameterContextEntity[];
     updateRequestEntity: ParameterContextUpdateRequestEntity | null;
     updateRequestParameterContextId: string | null;
+    postUpdateNavigation: string[] | null;
+    postUpdateNavigationBoundary: string[] | null;
+    postUpdateNavigationState: PostUpdateNavigationState | null;
     saving: boolean;
     loadedTimestamp: string;
     deleteUpdateRequestInitiated: boolean;
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.actions.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.actions.ts
index 11b50535e1e..c2117ddf6bb 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.actions.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.actions.ts
@@ -16,14 +16,18 @@
  */
 
 import { createAction, props } from '@ngrx/store';
-import { LoadParameterContextsResponse, SelectParameterContextRequest, 
GetEffectiveParameterContext } from './index';
+import {
+    LoadParameterContextsResponse,
+    SelectParameterContextRequest,
+    GetEffectiveParameterContext,
+    EditParameterContextRequest
+} from './index';
 import { PollParameterContextUpdateSuccess, SubmitParameterContextUpdate } 
from '../../../../state/shared';
 import {
     CreateParameterContextRequest,
     CreateParameterContextSuccess,
     DeleteParameterContextRequest,
-    DeleteParameterContextSuccess,
-    EditParameterContextRequest
+    DeleteParameterContextSuccess
 } from '../../../../ui/common/parameter-context';
 
 export const loadParameterContexts = createAction('[Parameter Context Listing] 
Load Parameter Contexts');
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.spec.ts
index 544b5ce1630..b689ee21bd6 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.spec.ts
@@ -15,14 +15,17 @@
  * limitations under the License.
  */
 
+import { EventEmitter } from '@angular/core';
 import { TestBed } from '@angular/core/testing';
-import { provideMockActions } from '@ngrx/effects/testing';
-import { Action } from '@ngrx/store';
-import { ReplaySubject, of, throwError } from 'rxjs';
-import { take } from 'rxjs/operators';
-import { provideMockStore } from '@ngrx/store/testing';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
 import { MatDialog } from '@angular/material/dialog';
 import { Router } from '@angular/router';
+import { Action } from '@ngrx/store';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { ReplaySubject, Subject, of, throwError } from 'rxjs';
+import { take } from 'rxjs/operators';
+import type { Mock, Mocked } from 'vitest';
 
 import { ParameterContextListingEffects } from 
'./parameter-context-listing.effects';
 import * as ParameterContextListingActions from 
'./parameter-context-listing.actions';
@@ -30,8 +33,13 @@ import { ParameterContextService } from 
'../../service/parameter-contexts.servic
 import { ErrorHelper } from '../../../../service/error-helper.service';
 import { Storage } from '@nifi/shared';
 import { initialState } from './parameter-context-listing.reducer';
+import { parameterContextsFeatureKey } from '../';
+import { parameterContextListingFeatureKey } from './index';
 import { ParameterContextUpdateRequest, ParameterContextUpdateRequestEntity } 
from '../../../../state/shared';
 import { HttpErrorResponse } from '@angular/common/http';
+import { EditParameterContext } from 
'../../../../ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component';
+import { SaveParameterContextChangesDialog } from 
'../../../../ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component';
+import { EditParameterContextUpdate } from 
'../../../../ui/common/parameter-context';
 
 describe('ParameterContextListingEffects', () => {
     interface SetupOptions {
@@ -39,6 +47,11 @@ describe('ParameterContextListingEffects', () => {
         updateRequestParameterContextId?: string | null;
         deleteUpdateRequestInitiated?: boolean;
         listStateOverride?: any;
+        postUpdateNavigation?: string[] | null;
+        postUpdateNavigationBoundary?: string[] | null;
+        postUpdateNavigationState?: { highlightedParameterName?: string } | 
null;
+        formDirty?: boolean;
+        formValid?: boolean;
     }
 
     let action$: ReplaySubject<Action>;
@@ -58,25 +71,99 @@ describe('ParameterContextListingEffects', () => {
         };
     }
 
+    function createParameterContext(id = 'pc-source') {
+        return {
+            id,
+            uri: `/parameter-contexts/${id}`,
+            revision: { version: 1 },
+            permissions: { canRead: true, canWrite: true },
+            component: {
+                id,
+                name: 'Source Context',
+                description: '',
+                parameters: [],
+                boundProcessGroups: [],
+                inheritedParameterContexts: []
+            }
+        };
+    }
+
     async function setup({
         updateRequest = null,
         updateRequestParameterContextId = null,
         deleteUpdateRequestInitiated = false,
-        listStateOverride
+        listStateOverride,
+        postUpdateNavigation = null,
+        postUpdateNavigationBoundary = null,
+        postUpdateNavigationState = null,
+        formDirty = false,
+        formValid = true
     }: SetupOptions = {}) {
+        const editParameterContext = new 
EventEmitter<EditParameterContextUpdate>();
+        const continuePostUpdateNavigation = new EventEmitter<void>();
+        const cancelUpdateRequest = new EventEmitter<void>();
+        const editDialogAfterClosed$ = new Subject<string | undefined>();
+        const saveChangesAfterClosed$ = new Subject<void>();
+        const save = new EventEmitter<void>();
+        const discard = new EventEmitter<void>();
+        const submitForm = vi.fn();
+
+        const editParameterContextForm = new FormGroup({
+            name: new FormControl(formValid ? 'Source Context' : '', 
Validators.required),
+            description: new FormControl(''),
+            parameters: new FormControl([]),
+            inheritedParameterContexts: new FormControl([])
+        });
+        if (formDirty) {
+            editParameterContextForm.markAsDirty();
+        }
+
+        const editDialogRef = {
+            componentInstance: {
+                updateRequest: undefined as unknown,
+                availableParameterContexts$: undefined as unknown,
+                saving$: undefined as unknown,
+                hasPendingPostUpdateNavigation$: undefined as unknown,
+                goToParameter: undefined as unknown,
+                createNewParameter: undefined as unknown,
+                editParameter: undefined as unknown,
+                editParameterContext,
+                continuePostUpdateNavigation,
+                cancelUpdateRequest,
+                editParameterContextForm,
+                submitForm
+            },
+            afterClosed: () => editDialogAfterClosed$.asObservable()
+        };
+
+        const saveChangesDialogRef = {
+            componentInstance: { save, discard },
+            afterClosed: () => saveChangesAfterClosed$.asObservable()
+        };
+
+        const dialogOpen = vi.fn((component: unknown) => {
+            if (component === SaveParameterContextChangesDialog) {
+                return saveChangesDialogRef;
+            }
+            return editDialogRef;
+        });
+
         await TestBed.configureTestingModule({
             providers: [
                 ParameterContextListingEffects,
                 provideMockActions(() => action$),
                 provideMockStore({
                     initialState: {
-                        parameterContexts: {
-                            parameterContextListing: {
+                        [parameterContextsFeatureKey]: {
+                            [parameterContextListingFeatureKey]: {
                                 ...initialState,
                                 ...listStateOverride,
                                 updateRequestEntity: updateRequest,
                                 updateRequestParameterContextId,
-                                deleteUpdateRequestInitiated
+                                deleteUpdateRequestInitiated,
+                                postUpdateNavigation,
+                                postUpdateNavigationBoundary,
+                                postUpdateNavigationState
                             }
                         }
                     }
@@ -89,8 +176,8 @@ describe('ParameterContextListingEffects', () => {
                         getParameterContexts: vi.fn()
                     }
                 },
-                { provide: MatDialog, useValue: { open: vi.fn() } },
-                { provide: Router, useValue: { navigate: vi.fn() } },
+                { provide: MatDialog, useValue: { open: dialogOpen } },
+                { provide: Router, useValue: { navigate: vi.fn(() => 
Promise.resolve(true)) } },
                 {
                     provide: ErrorHelper,
                     useValue: { getErrorString: vi.fn(), handleLoadingError: 
vi.fn(), fullScreenError: vi.fn() }
@@ -100,11 +187,47 @@ describe('ParameterContextListingEffects', () => {
         }).compileComponents();
 
         const effects = TestBed.inject(ParameterContextListingEffects);
-        const parameterContextService = 
TestBed.inject(ParameterContextService) as vi.Mocked<ParameterContextService>;
+        const parameterContextService = 
TestBed.inject(ParameterContextService) as Mocked<ParameterContextService>;
+        const dialog = TestBed.inject(MatDialog) as Mocked<MatDialog>;
+        const router = TestBed.inject(Router) as Mocked<Router>;
+        const store = TestBed.inject(MockStore);
+        const dispatchSpy = vi.spyOn(store, 'dispatch');
         action$ = new ReplaySubject<Action>();
 
         const errorHelper = TestBed.inject(ErrorHelper);
-        return { effects, parameterContextService, errorHelper };
+        return {
+            effects,
+            parameterContextService,
+            errorHelper,
+            dialog,
+            dialogOpen,
+            router,
+            store,
+            dispatchSpy,
+            editDialogRef,
+            saveChangesDialogRef,
+            save,
+            discard,
+            editParameterContext,
+            continuePostUpdateNavigation,
+            submitForm,
+            editDialogAfterClosed$,
+            saveChangesAfterClosed$
+        };
+    }
+
+    async function openEditDialog(
+        effects: ParameterContextListingEffects,
+        parameterContext = createParameterContext()
+    ) {
+        const subscription = effects.openParameterContextDialog$.subscribe();
+        action$.next(
+            ParameterContextListingActions.openParameterContextDialog({
+                request: { parameterContext }
+            })
+        );
+        await Promise.resolve();
+        return subscription;
     }
 
     beforeEach(() => {
@@ -115,6 +238,7 @@ describe('ParameterContextListingEffects', () => {
         if (action$) {
             action$.complete();
         }
+        TestBed.resetTestingModule();
     });
 
     it('should create', async () => {
@@ -128,7 +252,7 @@ describe('ParameterContextListingEffects', () => {
 
             
action$.next(ParameterContextListingActions.loadParameterContexts());
 
-            (parameterContextService.getParameterContexts as 
vi.Mock).mockReturnValueOnce(
+            (parameterContextService.getParameterContexts as 
Mock).mockReturnValueOnce(
                 of({ parameterContexts: [], currentTime: 't' })
             );
 
@@ -149,7 +273,7 @@ describe('ParameterContextListingEffects', () => {
             
action$.next(ParameterContextListingActions.loadParameterContexts());
 
             const error = new HttpErrorResponse({ status: 500 });
-            (parameterContextService.getParameterContexts as 
vi.Mock).mockImplementationOnce(() =>
+            (parameterContextService.getParameterContexts as 
Mock).mockImplementationOnce(() =>
                 throwError(() => error)
             );
 
@@ -173,7 +297,7 @@ describe('ParameterContextListingEffects', () => {
             
action$.next(ParameterContextListingActions.loadParameterContexts());
 
             const error = new HttpErrorResponse({ status: 500 });
-            (parameterContextService.getParameterContexts as 
vi.Mock).mockImplementationOnce(() =>
+            (parameterContextService.getParameterContexts as 
Mock).mockImplementationOnce(() =>
                 throwError(() => error)
             );
 
@@ -342,8 +466,251 @@ describe('ParameterContextListingEffects', () => {
 
             
action$.next(ParameterContextListingActions.pollParameterContextUpdateRequestSuccess({
 response }));
 
-            // Since the effect is synchronous with filter, we can check 
immediately
             expect(emissions).toEqual([]);
         });
     });
+
+    describe('openParameterContextDialog$ goToParameter', () => {
+        it('should navigate immediately when the form is clean', async () => {
+            const { effects, dialogOpen, router, editDialogRef } = await 
setup({ formDirty: false });
+            const subscription = await openEditDialog(effects);
+
+            expect(dialogOpen).toHaveBeenCalledWith(EditParameterContext, 
expect.anything());
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+
+            
expect(router.navigate).toHaveBeenCalledWith(['/parameter-contexts', 
'pc-inherited', 'edit'], {
+                state: {
+                    backNavigation: {
+                        route: ['/parameter-contexts', 'pc-source', 'edit'],
+                        routeBoundary: ['/parameter-contexts'],
+                        context: 'Parameter Context'
+                    },
+                    highlightedParameterName: 'inherited-param'
+                }
+            });
+
+            subscription.unsubscribe();
+        });
+
+        it('should open the save-changes dialog with canSave true when the 
form is dirty and valid', async () => {
+            const { effects, dialogOpen, editDialogRef } = await setup({ 
formDirty: true, formValid: true });
+            const subscription = await openEditDialog(effects);
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+
+            expect(dialogOpen).toHaveBeenCalledWith(
+                SaveParameterContextChangesDialog,
+                expect.objectContaining({
+                    data: {
+                        destination: 'Parameter',
+                        canSave: true
+                    }
+                })
+            );
+            subscription.unsubscribe();
+        });
+
+        it('should open the save-changes dialog with canSave false when the 
form is dirty and invalid', async () => {
+            const { effects, dialogOpen, editDialogRef } = await setup({ 
formDirty: true, formValid: false });
+            const subscription = await openEditDialog(effects);
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+
+            expect(dialogOpen).toHaveBeenCalledWith(
+                SaveParameterContextChangesDialog,
+                expect.objectContaining({
+                    data: {
+                        destination: 'Parameter',
+                        canSave: false
+                    }
+                })
+            );
+            subscription.unsubscribe();
+        });
+
+        it('should submitForm with postUpdateNavigation when dirty and Save is 
chosen', async () => {
+            const { effects, editDialogRef, submitForm, save, router } = await 
setup({
+                formDirty: true,
+                formValid: true
+            });
+            const subscription = await openEditDialog(effects);
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+            save.next();
+
+            expect(submitForm).toHaveBeenCalledWith(
+                ['/parameter-contexts', 'pc-inherited', 'edit'],
+                ['/parameter-contexts'],
+                { highlightedParameterName: 'inherited-param' }
+            );
+            expect(router.navigate).not.toHaveBeenCalled();
+            subscription.unsubscribe();
+        });
+
+        it('should not submitForm when dirty, invalid, and Save is emitted', 
async () => {
+            const { effects, editDialogRef, submitForm, save, router } = await 
setup({
+                formDirty: true,
+                formValid: false
+            });
+            const subscription = await openEditDialog(effects);
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+            save.next();
+
+            expect(submitForm).not.toHaveBeenCalled();
+            expect(router.navigate).not.toHaveBeenCalled();
+            subscription.unsubscribe();
+        });
+
+        it("should navigate without submit when dirty and Don't Save is 
chosen", async () => {
+            const { effects, editDialogRef, submitForm, discard, router } = 
await setup({
+                formDirty: true,
+                formValid: true
+            });
+            const subscription = await openEditDialog(effects);
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+            discard.next();
+
+            expect(submitForm).not.toHaveBeenCalled();
+            
expect(router.navigate).toHaveBeenCalledWith(['/parameter-contexts', 
'pc-inherited', 'edit'], {
+                state: {
+                    backNavigation: {
+                        route: ['/parameter-contexts', 'pc-source', 'edit'],
+                        routeBoundary: ['/parameter-contexts'],
+                        context: 'Parameter Context'
+                    },
+                    highlightedParameterName: 'inherited-param'
+                }
+            });
+            subscription.unsubscribe();
+        });
+
+        it("should navigate without submit when dirty, invalid, and Don't Save 
is chosen", async () => {
+            const { effects, editDialogRef, submitForm, discard, router } = 
await setup({
+                formDirty: true,
+                formValid: false
+            });
+            const subscription = await openEditDialog(effects);
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+            discard.next();
+
+            expect(submitForm).not.toHaveBeenCalled();
+            
expect(router.navigate).toHaveBeenCalledWith(['/parameter-contexts', 
'pc-inherited', 'edit'], {
+                state: {
+                    backNavigation: {
+                        route: ['/parameter-contexts', 'pc-source', 'edit'],
+                        routeBoundary: ['/parameter-contexts'],
+                        context: 'Parameter Context'
+                    },
+                    highlightedParameterName: 'inherited-param'
+                }
+            });
+            subscription.unsubscribe();
+        });
+
+        it('should neither submit nor navigate when dirty and the save-changes 
dialog is closed without an action', async () => {
+            const { effects, editDialogRef, submitForm, router, 
saveChangesAfterClosed$ } = await setup({
+                formDirty: true,
+                formValid: true
+            });
+            const subscription = await openEditDialog(effects);
+
+            (editDialogRef.componentInstance.goToParameter as (id: string, 
name: string) => void)(
+                'pc-inherited',
+                'inherited-param'
+            );
+            saveChangesAfterClosed$.next();
+
+            expect(submitForm).not.toHaveBeenCalled();
+            expect(router.navigate).not.toHaveBeenCalled();
+            subscription.unsubscribe();
+        });
+
+        it('should unwrap editParameterContext emit into 
submitParameterContextUpdateRequest', async () => {
+            const { effects, editDialogRef, dispatchSpy } = await setup();
+            const subscription = await openEditDialog(effects);
+
+            const update: EditParameterContextUpdate = {
+                payload: { id: 'pc-source', component: { id: 'pc-source' } },
+                postUpdateNavigation: ['/parameter-contexts', 'pc-inherited', 
'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'inherited-param' }
+            };
+            editDialogRef.componentInstance.editParameterContext.next(update);
+
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                
ParameterContextListingActions.submitParameterContextUpdateRequest({
+                    request: {
+                        id: 'pc-source',
+                        payload: update.payload,
+                        postUpdateNavigation: update.postUpdateNavigation,
+                        postUpdateNavigationBoundary: 
update.postUpdateNavigationBoundary,
+                        postUpdateNavigationState: 
update.postUpdateNavigationState
+                    }
+                })
+            );
+            subscription.unsubscribe();
+        });
+
+        it('should navigate when continuePostUpdateNavigation is emitted with 
pending navigation', async () => {
+            const { effects, continuePostUpdateNavigation, router } = await 
setup({
+                postUpdateNavigation: ['/parameter-contexts', 'pc-inherited', 
'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'inherited-param' }
+            });
+            const subscription = await openEditDialog(effects);
+
+            continuePostUpdateNavigation.next();
+            await Promise.resolve();
+
+            
expect(router.navigate).toHaveBeenCalledWith(['/parameter-contexts', 
'pc-inherited', 'edit'], {
+                state: {
+                    backNavigation: {
+                        route: ['/parameter-contexts', 'pc-source', 'edit'],
+                        routeBoundary: ['/parameter-contexts'],
+                        context: 'Parameter Context'
+                    },
+                    highlightedParameterName: 'inherited-param'
+                }
+            });
+            subscription.unsubscribe();
+        });
+
+        it('should not navigate when continuePostUpdateNavigation is emitted 
without pending navigation', async () => {
+            const { effects, continuePostUpdateNavigation, router } = await 
setup({
+                postUpdateNavigation: null
+            });
+            const subscription = await openEditDialog(effects);
+
+            continuePostUpdateNavigation.next();
+            await Promise.resolve();
+
+            expect(router.navigate).not.toHaveBeenCalled();
+            subscription.unsubscribe();
+        });
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts
index 74b9a005cb8..493c88a3071 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts
@@ -34,7 +34,7 @@ import {
     takeUntil,
     tap
 } from 'rxjs';
-import { MatDialog } from '@angular/material/dialog';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
 import { Store } from '@ngrx/store';
 import { NiFiState } from '../../../../state';
 import { Router } from '@angular/router';
@@ -46,9 +46,18 @@ import {
     selectSaving,
     selectUpdateRequest,
     selectUpdateRequestParameterContextId,
-    selectDeleteUpdateRequestInitiated
+    selectDeleteUpdateRequestInitiated,
+    selectHasPendingPostUpdateNavigation,
+    selectPostUpdateNavigation,
+    selectPostUpdateNavigationBoundary,
+    selectPostUpdateNavigationState
 } from './parameter-context-listing.selectors';
-import { EditParameterRequest, EditParameterResponse, 
ParameterContextUpdateRequest } from '../../../../state/shared';
+import {
+    EditParameterRequest,
+    EditParameterResponse,
+    ParameterContextUpdateRequest,
+    PostUpdateNavigationState
+} from '../../../../state/shared';
 import { EditParameterDialog } from 
'../../../../ui/common/edit-parameter-dialog/edit-parameter-dialog.component';
 import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
 import { ErrorHelper } from '../../../../service/error-helper.service';
@@ -58,6 +67,8 @@ import { BackNavigation } from '../../../../state/navigation';
 import { isDefinedAndNotNull, MEDIUM_DIALOG, SMALL_DIALOG, XL_DIALOG, 
NiFiCommon, Storage } from '@nifi/shared';
 import { ErrorContextKey } from '../../../../state/error';
 import { EditParameterContext } from 
'../../../../ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component';
+import { SaveParameterContextChangesDialog } from 
'../../../../ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component';
+import { EditParameterContextUpdate } from 
'../../../../ui/common/parameter-context';
 
 @Injectable()
 export class ParameterContextListingEffects {
@@ -295,7 +306,8 @@ export class ParameterContextListingEffects {
                     map((response) =>
                         
ParameterContextListingActions.openParameterContextDialog({
                             request: {
-                                parameterContext: response
+                                parameterContext: response,
+                                highlightedParameterName: 
request.highlightedParameterName
                             }
                         })
                     ),
@@ -326,7 +338,8 @@ export class ParameterContextListingEffects {
                     const editDialogReference = 
this.dialog.open(EditParameterContext, {
                         ...XL_DIALOG,
                         data: {
-                            parameterContext: request.parameterContext
+                            parameterContext: request.parameterContext,
+                            highlightedParameterName: 
request.highlightedParameterName
                         }
                     });
 
@@ -338,6 +351,27 @@ export class ParameterContextListingEffects {
                             map((parameterContexts) => 
parameterContexts.filter((pc) => pc.id != parameterContextId))
                         );
                     editDialogReference.componentInstance.saving$ = 
this.store.select(selectSaving);
+                    
editDialogReference.componentInstance.hasPendingPostUpdateNavigation$ = 
this.store.select(
+                        selectHasPendingPostUpdateNavigation
+                    );
+
+                    editDialogReference.componentInstance.goToParameter = (
+                        inheritedParameterContextId: string,
+                        parameterName: string
+                    ) => {
+                        const commandBoundary: string[] = 
['/parameter-contexts'];
+                        const commands: string[] = [...commandBoundary, 
inheritedParameterContextId, 'edit'];
+                        this.goToInheritedParameter(
+                            editDialogReference,
+                            parameterContextId,
+                            commands,
+                            commandBoundary,
+                            'Parameter',
+                            {
+                                highlightedParameterName: parameterName
+                            }
+                        );
+                    };
 
                     editDialogReference.componentInstance.createNewParameter = 
(
                         existingParameters: string[]
@@ -393,17 +427,46 @@ export class ParameterContextListingEffects {
 
                     editDialogReference.componentInstance.editParameterContext
                         .pipe(takeUntil(editDialogReference.afterClosed()))
-                        .subscribe((payload: any) => {
+                        .subscribe((updateRequest: EditParameterContextUpdate) 
=> {
                             this.store.dispatch(
                                 
ParameterContextListingActions.submitParameterContextUpdateRequest({
                                     request: {
                                         id: parameterContextId,
-                                        payload
+                                        payload: updateRequest.payload,
+                                        postUpdateNavigation: 
updateRequest.postUpdateNavigation,
+                                        postUpdateNavigationBoundary: 
updateRequest.postUpdateNavigationBoundary,
+                                        postUpdateNavigationState: 
updateRequest.postUpdateNavigationState
                                     }
                                 })
                             );
                         });
 
+                    
editDialogReference.componentInstance.continuePostUpdateNavigation
+                        .pipe(
+                            takeUntil(editDialogReference.afterClosed()),
+                            switchMap(() =>
+                                
this.store.select(selectPostUpdateNavigation).pipe(
+                                    take(1),
+                                    concatLatestFrom(() => [
+                                        
this.store.select(selectPostUpdateNavigationBoundary),
+                                        
this.store.select(selectPostUpdateNavigationState)
+                                    ])
+                                )
+                            )
+                        )
+                        .subscribe(
+                            ([postUpdateNavigation, 
postUpdateNavigationBoundary, postUpdateNavigationState]) => {
+                                if (postUpdateNavigation) {
+                                    this.navigateToInheritedParameter(
+                                        parameterContextId,
+                                        postUpdateNavigation,
+                                        postUpdateNavigationBoundary ?? 
['/parameter-contexts'],
+                                        postUpdateNavigationState ?? undefined
+                                    );
+                                }
+                            }
+                        );
+
                     editDialogReference.componentInstance.cancelUpdateRequest
                         .pipe(takeUntil(editDialogReference.afterClosed()))
                         .subscribe(() => {
@@ -654,4 +717,70 @@ export class ParameterContextListingEffects {
             ),
         { dispatch: false }
     );
+
+    /**
+     * Navigates to the supplied route, recording back navigation to the 
Parameter Context that was being edited.
+     */
+    private navigateToInheritedParameter(
+        sourceParameterContextId: string,
+        commands: string[],
+        commandBoundary: string[],
+        navigationState?: PostUpdateNavigationState
+    ): void {
+        this.router.navigate(commands, {
+            state: {
+                backNavigation: {
+                    route: ['/parameter-contexts', sourceParameterContextId, 
'edit'],
+                    routeBoundary: commandBoundary,
+                    context: 'Parameter Context'
+                } as BackNavigation,
+                ...navigationState
+            }
+        });
+    }
+
+    /**
+     * Navigates to an inherited Parameter. When the Parameter Context edit 
form has unsaved changes the user is
+     * prompted to save or discard them first. Saving defers the navigation 
until the update request completes and
+     * the user opts to continue.
+     */
+    private goToInheritedParameter(
+        editDialogReference: MatDialogRef<EditParameterContext>,
+        sourceParameterContextId: string,
+        commands: string[],
+        commandBoundary: string[],
+        destination: string,
+        navigationState?: PostUpdateNavigationState
+    ): void {
+        const editParameterContextForm = 
editDialogReference.componentInstance.editParameterContextForm;
+
+        if (!editParameterContextForm.dirty) {
+            this.navigateToInheritedParameter(sourceParameterContextId, 
commands, commandBoundary, navigationState);
+            return;
+        }
+
+        const saveChangesDialogReference = 
this.dialog.open(SaveParameterContextChangesDialog, {
+            ...SMALL_DIALOG,
+            data: {
+                destination,
+                canSave: editParameterContextForm.valid
+            }
+        });
+
+        // Defense in depth: Save is disabled when invalid, but still guard 
before submit.
+        saveChangesDialogReference.componentInstance.save
+            .pipe(takeUntil(saveChangesDialogReference.afterClosed()), take(1))
+            .subscribe(() => {
+                if (!editParameterContextForm.valid) {
+                    return;
+                }
+                editDialogReference.componentInstance.submitForm(commands, 
commandBoundary, navigationState);
+            });
+
+        saveChangesDialogReference.componentInstance.discard
+            .pipe(takeUntil(saveChangesDialogReference.afterClosed()), take(1))
+            .subscribe(() => {
+                this.navigateToInheritedParameter(sourceParameterContextId, 
commands, commandBoundary, navigationState);
+            });
+    }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.spec.ts
new file mode 100644
index 00000000000..e70364b39e9
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.spec.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { initialState, parameterContextListingReducer } from 
'./parameter-context-listing.reducer';
+import {
+    deleteParameterContextUpdateRequestSuccess,
+    editParameterContextComplete,
+    parameterContextListingBannerApiError,
+    submitParameterContextUpdateRequest
+} from './parameter-context-listing.actions';
+import { ParameterContextListingState } from './index';
+
+describe('ParameterContextListing Reducer', () => {
+    describe('submitParameterContextUpdateRequest', () => {
+        it('should persist postUpdateNavigation fields from the request', () 
=> {
+            const result = parameterContextListingReducer(
+                initialState,
+                submitParameterContextUpdateRequest({
+                    request: {
+                        id: 'pc-1',
+                        payload: {},
+                        postUpdateNavigation: ['/parameter-contexts', 
'inherited-id', 'edit'],
+                        postUpdateNavigationBoundary: ['/parameter-contexts'],
+                        postUpdateNavigationState: { highlightedParameterName: 
'param-a' }
+                    }
+                })
+            );
+
+            expect(result.saving).toBe(true);
+            expect(result.updateRequestParameterContextId).toBe('pc-1');
+            
expect(result.postUpdateNavigation).toEqual(['/parameter-contexts', 
'inherited-id', 'edit']);
+            
expect(result.postUpdateNavigationBoundary).toEqual(['/parameter-contexts']);
+            expect(result.postUpdateNavigationState).toEqual({ 
highlightedParameterName: 'param-a' });
+        });
+
+        it('should clear postUpdateNavigation fields when not provided on 
submit', () => {
+            const stateWithPendingNav: ParameterContextListingState = {
+                ...initialState,
+                postUpdateNavigation: ['/parameter-contexts', 'old-id', 
'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 'old' }
+            };
+
+            const result = parameterContextListingReducer(
+                stateWithPendingNav,
+                submitParameterContextUpdateRequest({
+                    request: {
+                        id: 'pc-1',
+                        payload: {}
+                    }
+                })
+            );
+
+            expect(result.postUpdateNavigation).toBeNull();
+            expect(result.postUpdateNavigationBoundary).toBeNull();
+            expect(result.postUpdateNavigationState).toBeNull();
+        });
+    });
+
+    describe('deleteParameterContextUpdateRequestSuccess', () => {
+        it('should keep postUpdateNavigation fields for the review CTA', () => 
{
+            const stateWithPendingNav: ParameterContextListingState = {
+                ...initialState,
+                saving: true,
+                postUpdateNavigation: ['/parameter-contexts', 'inherited-id', 
'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'param-a' },
+                updateRequestEntity: {
+                    request: {
+                        requestId: 'req-1',
+                        uri: '',
+                        lastUpdated: '',
+                        complete: true,
+                        percentComponent: 100,
+                        state: 'Complete',
+                        updateSteps: [],
+                        referencingComponents: []
+                    },
+                    parameterContextRevision: { version: 1 }
+                }
+            };
+
+            const result = parameterContextListingReducer(
+                stateWithPendingNav,
+                deleteParameterContextUpdateRequestSuccess({
+                    response: {
+                        requestEntity: stateWithPendingNav.updateRequestEntity!
+                    }
+                })
+            );
+
+            expect(result.saving).toBe(false);
+            
expect(result.postUpdateNavigation).toEqual(['/parameter-contexts', 
'inherited-id', 'edit']);
+            
expect(result.postUpdateNavigationBoundary).toEqual(['/parameter-contexts']);
+            expect(result.postUpdateNavigationState).toEqual({ 
highlightedParameterName: 'param-a' });
+        });
+    });
+
+    describe('parameterContextListingBannerApiError', () => {
+        it('should clear postUpdateNavigation fields on error', () => {
+            const stateWithPendingNav: ParameterContextListingState = {
+                ...initialState,
+                saving: true,
+                postUpdateNavigation: ['/parameter-contexts', 'inherited-id', 
'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'param-a' }
+            };
+
+            const result = parameterContextListingReducer(
+                stateWithPendingNav,
+                parameterContextListingBannerApiError({ error: 'boom' })
+            );
+
+            expect(result.saving).toBe(false);
+            expect(result.postUpdateNavigation).toBeNull();
+            expect(result.postUpdateNavigationBoundary).toBeNull();
+            expect(result.postUpdateNavigationState).toBeNull();
+        });
+    });
+
+    describe('editParameterContextComplete', () => {
+        it('should clear postUpdateNavigation fields when the dialog closes', 
() => {
+            const stateWithPendingNav: ParameterContextListingState = {
+                ...initialState,
+                postUpdateNavigation: ['/parameter-contexts', 'inherited-id', 
'edit'],
+                postUpdateNavigationBoundary: ['/parameter-contexts'],
+                postUpdateNavigationState: { highlightedParameterName: 
'param-a' }
+            };
+
+            const result = parameterContextListingReducer(stateWithPendingNav, 
editParameterContextComplete());
+
+            expect(result.postUpdateNavigation).toBeNull();
+            expect(result.postUpdateNavigationBoundary).toBeNull();
+            expect(result.postUpdateNavigationState).toBeNull();
+        });
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts
index 018a78c0356..c06626dd7ab 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts
@@ -41,6 +41,9 @@ export const initialState: ParameterContextListingState = {
     parameterContexts: [],
     updateRequestEntity: null,
     updateRequestParameterContextId: null,
+    postUpdateNavigation: null,
+    postUpdateNavigationBoundary: null,
+    postUpdateNavigationState: null,
     saving: false,
     loadedTimestamp: '',
     deleteUpdateRequestInitiated: false,
@@ -66,7 +69,10 @@ export const parameterContextListingReducer = createReducer(
     })),
     on(parameterContextListingSnackbarApiError, 
parameterContextListingBannerApiError, (state) => ({
         ...state,
-        saving: false
+        saving: false,
+        postUpdateNavigation: null,
+        postUpdateNavigationBoundary: null,
+        postUpdateNavigationState: null
     })),
     on(createParameterContext, (state) => ({
         ...state,
@@ -81,7 +87,10 @@ export const parameterContextListingReducer = createReducer(
     on(submitParameterContextUpdateRequest, (state, { request }) => ({
         ...state,
         saving: true,
-        updateRequestParameterContextId: request.id
+        updateRequestParameterContextId: request.id,
+        postUpdateNavigation: request.postUpdateNavigation ?? null,
+        postUpdateNavigationBoundary: request.postUpdateNavigationBoundary ?? 
null,
+        postUpdateNavigationState: request.postUpdateNavigationState ?? null
     })),
     on(
         submitParameterContextUpdateRequestSuccess,
@@ -132,6 +141,10 @@ export const parameterContextListingReducer = 
createReducer(
                 draftState.saving = false;
                 draftState.deleteUpdateRequestInitiated = false;
             }
+
+            draftState.postUpdateNavigation = null;
+            draftState.postUpdateNavigationBoundary = null;
+            draftState.postUpdateNavigationState = null;
         });
     }),
     on(deleteParameterContextSuccess, (state, { response }) => {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts
index bc181a2e3a9..6797fe172f6 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts
@@ -74,3 +74,23 @@ export const selectDeleteUpdateRequestInitiated = 
createSelector(
     selectParameterContextListingState,
     (state: ParameterContextListingState) => state.deleteUpdateRequestInitiated
 );
+
+export const selectPostUpdateNavigation = createSelector(
+    selectParameterContextListingState,
+    (state: ParameterContextListingState) => state.postUpdateNavigation
+);
+
+export const selectPostUpdateNavigationBoundary = createSelector(
+    selectParameterContextListingState,
+    (state: ParameterContextListingState) => state.postUpdateNavigationBoundary
+);
+
+export const selectPostUpdateNavigationState = createSelector(
+    selectParameterContextListingState,
+    (state: ParameterContextListingState) => state.postUpdateNavigationState
+);
+
+export const selectHasPendingPostUpdateNavigation = createSelector(
+    selectPostUpdateNavigation,
+    (postUpdateNavigation) => postUpdateNavigation != null && 
postUpdateNavigation.length > 0
+);
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.spec.ts
index 3e2297ee144..ebf29139b6b 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.spec.ts
@@ -18,34 +18,136 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { ParameterContextListing } from 
'./parameter-context-listing.component';
-import { provideMockStore } from '@ngrx/store/testing';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
 import { initialState } from 
'../../state/parameter-context-listing/parameter-context-listing.reducer';
 import { parameterContextListingFeatureKey } from 
'../../state/parameter-context-listing';
 import { parameterContextsFeatureKey } from '../../state';
+import { selectSingleEditedParameterContext } from 
'../../state/parameter-context-listing/parameter-context-listing.selectors';
+import { getEffectiveParameterContextAndOpenDialog } from 
'../../state/parameter-context-listing/parameter-context-listing.actions';
+import { Navigation, Router } from '@angular/router';
+import { ParameterContextEntity } from '../../../../state/shared';
 
 describe('ParameterContextListing', () => {
     let component: ParameterContextListing;
     let fixture: ComponentFixture<ParameterContextListing>;
 
-    beforeEach(() => {
+    const parameterContext: ParameterContextEntity = {
+        revision: {
+            version: 0
+        },
+        id: '1234',
+        uri: 'https://localhost:4200/nifi-api/parameter-contexts/1234',
+        permissions: {
+            canRead: true,
+            canWrite: true
+        },
+        component: {
+            name: 'params 1',
+            description: '',
+            parameters: [],
+            boundProcessGroups: [],
+            inheritedParameterContexts: [],
+            id: '1234'
+        }
+    };
+
+    let lastSuccessfulNavigation: Navigation | null = null;
+
+    const configureTestBed = (editedParameterContextId: string | null, 
parameterContexts: ParameterContextEntity[]) => {
         TestBed.configureTestingModule({
             imports: [ParameterContextListing],
             providers: [
                 provideMockStore({
                     initialState: {
                         [parameterContextsFeatureKey]: {
-                            [parameterContextListingFeatureKey]: initialState
+                            [parameterContextListingFeatureKey]: {
+                                ...initialState,
+                                parameterContexts
+                            }
                         }
                     }
-                })
+                }),
+                {
+                    provide: Router,
+                    useValue: {
+                        lastSuccessfulNavigation: () => 
lastSuccessfulNavigation
+                    }
+                }
             ]
         });
+
+        const store: MockStore = TestBed.inject(MockStore);
+        store.overrideSelector(selectSingleEditedParameterContext, 
editedParameterContextId);
+
+        return store;
+    };
+
+    beforeEach(() => {
+        lastSuccessfulNavigation = null;
+    });
+
+    it('should create', () => {
+        configureTestBed(null, []);
         fixture = TestBed.createComponent(ParameterContextListing);
         component = fixture.componentInstance;
         fixture.detectChanges();
-    });
 
-    it('should create', () => {
         expect(component).toBeTruthy();
     });
+
+    describe('when the edit route is active', () => {
+        it('should open the dialog with the highlighted parameter from the 
navigation state', () => {
+            lastSuccessfulNavigation = {
+                extras: {
+                    state: {
+                        highlightedParameterName: 'param A'
+                    }
+                }
+            } as unknown as Navigation;
+
+            const store = configureTestBed(parameterContext.id, 
[parameterContext]);
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+
+            fixture = TestBed.createComponent(ParameterContextListing);
+            fixture.detectChanges();
+
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                getEffectiveParameterContextAndOpenDialog({
+                    request: {
+                        id: parameterContext.id,
+                        highlightedParameterName: 'param A'
+                    }
+                })
+            );
+        });
+
+        it('should open the dialog without a highlighted parameter when the 
navigation state is absent', () => {
+            const store = configureTestBed(parameterContext.id, 
[parameterContext]);
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+
+            fixture = TestBed.createComponent(ParameterContextListing);
+            fixture.detectChanges();
+
+            expect(dispatchSpy).toHaveBeenCalledWith(
+                getEffectiveParameterContextAndOpenDialog({
+                    request: {
+                        id: parameterContext.id,
+                        highlightedParameterName: undefined
+                    }
+                })
+            );
+        });
+
+        it('should not open the dialog until the parameter context is loaded', 
() => {
+            const store = configureTestBed(parameterContext.id, []);
+            const dispatchSpy = vi.spyOn(store, 'dispatch');
+
+            fixture = TestBed.createComponent(ParameterContextListing);
+            fixture.detectChanges();
+
+            expect(dispatchSpy).not.toHaveBeenCalledWith(
+                expect.objectContaining({ type: 
getEffectiveParameterContextAndOpenDialog.type })
+            );
+        });
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.ts
index 182989ffac2..eb5d61e1456 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-context-listing.component.ts
@@ -17,6 +17,7 @@
 
 import { Component, OnInit, inject } from '@angular/core';
 import { Store } from '@ngrx/store';
+import { Router } from '@angular/router';
 import { ParameterContextListingState } from 
'../../state/parameter-context-listing';
 import {
     selectContext,
@@ -34,7 +35,7 @@ import {
     selectParameterContext
 } from 
'../../state/parameter-context-listing/parameter-context-listing.actions';
 import { initialState } from 
'../../state/parameter-context-listing/parameter-context-listing.reducer';
-import { filter, switchMap, take } from 'rxjs';
+import { filter, map, switchMap, take } from 'rxjs';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { selectCurrentUser } from 
'../../../../state/current-user/current-user.selectors';
 import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.selectors';
@@ -52,6 +53,7 @@ import { ParameterContextTable } from 
'./parameter-context-table/parameter-conte
 })
 export class ParameterContextListing implements OnInit {
     private store = inject<Store<ParameterContextListingState>>(Store);
+    private router = inject(Router);
 
     parameterContextListingState$ = 
this.store.select(selectParameterContextListingState);
     selectedParameterContextId$ = 
this.store.select(selectParameterContextIdFromRoute);
@@ -63,20 +65,30 @@ export class ParameterContextListing implements OnInit {
             .select(selectSingleEditedParameterContext)
             .pipe(
                 filter((id: string) => id != null),
-                switchMap((id: string) =>
+                // capture the highlighted parameter when the route resolves, 
before waiting on the parameter
+                // context to load, so a subsequent navigation cannot be 
attributed to this request
+                map((id: string) => ({
+                    id,
+                    highlightedParameterName: 
this.router.lastSuccessfulNavigation()?.extras?.state?.[
+                        'highlightedParameterName'
+                    ] as string | undefined
+                })),
+                switchMap(({ id, highlightedParameterName }) =>
                     this.store.select(selectContext(id)).pipe(
                         filter((entity) => entity != null),
-                        take(1)
+                        take(1),
+                        map((entity) => ({ entity, highlightedParameterName }))
                     )
                 ),
                 takeUntilDestroyed()
             )
-            .subscribe((entity) => {
+            .subscribe(({ entity, highlightedParameterName }) => {
                 if (entity) {
                     this.store.dispatch(
                         getEffectiveParameterContextAndOpenDialog({
                             request: {
-                                id: entity.id
+                                id: entity.id,
+                                highlightedParameterName
                             }
                         })
                     );
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.html
index eadeaaf17e9..004fd55d345 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.html
@@ -137,7 +137,7 @@
                                 }
                                 <mat-menu #actionMenu="matMenu" 
xPosition="before">
                                     @if (canGoToParameter(item)) {
-                                        <button mat-menu-item 
[routerLink]="getParameterLink(item)">
+                                        <button aria-label="Go to" 
mat-menu-item (click)="goToParameterClicked(item)">
                                             <i class="fa fa-long-arrow-right 
primary-color mr-2"></i>
                                             Go to
                                         </button>
@@ -171,6 +171,7 @@
                         *matRowDef="let row; let even = even; columns: 
displayedColumns"
                         (click)="selectParameter(row)"
                         (dblclick)="doubleClicked(row)"
+                        
[attr.data-parameter-name]="row.originalEntity.parameter.name"
                         [class.selected]="isSelected(row)"
                         [class.even]="even"></tr>
                 </table>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.spec.ts
index 4fdd000a73b..a4e168632e5 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.spec.ts
@@ -422,6 +422,167 @@ describe('ParameterTable', () => {
         });
     });
 
+    describe('goToParameterClicked', () => {
+        it('should invoke goToParameter with inherited parameter context id 
and parameter name', () => {
+            const goToParameterSpy = vi.fn();
+            component.goToParameter = goToParameterSpy;
+
+            const item: ParameterItem = {
+                added: false,
+                dirty: false,
+                deleted: false,
+                originalEntity: {
+                    parameter: {
+                        name: 'inherited-param',
+                        value: 'value',
+                        description: 'asdf',
+                        sensitive: false,
+                        inherited: true,
+                        parameterContext: {
+                            id: 'inherited-context-id',
+                            permissions: {
+                                canRead: true,
+                                canWrite: true
+                            },
+                            component: {
+                                id: 'inherited-context-id',
+                                name: 'Inherited Context'
+                            }
+                        }
+                    },
+                    canWrite: true
+                }
+            };
+
+            component.goToParameterClicked(item);
+
+            
expect(goToParameterSpy).toHaveBeenCalledWith('inherited-context-id', 
'inherited-param');
+        });
+
+        it('should not invoke goToParameter when parameterContext is missing', 
() => {
+            const goToParameterSpy = vi.fn();
+            component.goToParameter = goToParameterSpy;
+
+            const item: ParameterItem = {
+                added: false,
+                dirty: false,
+                deleted: false,
+                originalEntity: {
+                    parameter: {
+                        name: 'inherited-param',
+                        value: 'value',
+                        description: 'asdf',
+                        sensitive: false,
+                        inherited: true
+                    },
+                    canWrite: true
+                }
+            };
+
+            component.goToParameterClicked(item);
+
+            expect(goToParameterSpy).not.toHaveBeenCalled();
+        });
+
+        it('should no-op goToParameterClicked when goToParameter callback is 
not supplied', () => {
+            expect(component.goToParameter).toBeUndefined();
+
+            const item: ParameterItem = {
+                added: false,
+                dirty: false,
+                deleted: false,
+                originalEntity: {
+                    parameter: {
+                        name: 'inherited-param',
+                        value: 'value',
+                        description: 'asdf',
+                        sensitive: false,
+                        inherited: true,
+                        parameterContext: {
+                            id: 'inherited-context-id',
+                            permissions: {
+                                canRead: true,
+                                canWrite: true
+                            },
+                            component: {
+                                id: 'inherited-context-id',
+                                name: 'Inherited Context'
+                            }
+                        }
+                    },
+                    canWrite: true
+                }
+            };
+
+            expect(() => component.goToParameterClicked(item)).not.toThrow();
+        });
+
+        it('canGoToParameter returns false when goToParameter callback is not 
supplied', () => {
+            expect(component.goToParameter).toBeUndefined();
+
+            const item: ParameterItem = {
+                added: false,
+                dirty: false,
+                deleted: false,
+                originalEntity: {
+                    parameter: {
+                        name: 'inherited-param',
+                        value: 'value',
+                        description: 'asdf',
+                        sensitive: false,
+                        inherited: true,
+                        parameterContext: {
+                            id: 'inherited-context-id',
+                            permissions: {
+                                canRead: true,
+                                canWrite: true
+                            },
+                            component: {
+                                id: 'inherited-context-id',
+                                name: 'Inherited Context'
+                            }
+                        }
+                    },
+                    canWrite: true
+                }
+            };
+
+            expect(component.canGoToParameter(item)).toBe(false);
+        });
+    });
+
+    describe('highlightedParameterName', () => {
+        it('selects the row whose name matches highlightedParameterName', () 
=> {
+            component.writeValue([
+                {
+                    parameter: { name: 'alpha', value: 'v', description: '', 
sensitive: false },
+                    canWrite: true
+                },
+                {
+                    parameter: { name: 'beta', value: 'v', description: '', 
sensitive: false },
+                    canWrite: true
+                }
+            ]);
+            component.highlightedParameterName = 'beta';
+            component.ngAfterViewInit();
+
+            
expect(component.selectedItem?.originalEntity.parameter.name).toBe('beta');
+        });
+
+        it('does not select any row when highlightedParameterName is 
undefined', () => {
+            component.writeValue([
+                {
+                    parameter: { name: 'alpha', value: 'v', description: '', 
sensitive: false },
+                    canWrite: true
+                }
+            ]);
+            component.highlightedParameterName = undefined;
+            component.ngAfterViewInit();
+
+            expect(component.selectedItem).toBeNull();
+        });
+    });
+
     describe('canEdit', () => {
         it('should consider inherited and not modified as unable to edit', () 
=> {
             const item: ParameterItem = {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.ts
index 70f615a1e71..ea751fda98d 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component.ts
@@ -15,13 +15,22 @@
  * limitations under the License.
  */
 
-import { AfterViewInit, ChangeDetectorRef, Component, forwardRef, Input, 
inject } from '@angular/core';
+import {
+    AfterViewInit,
+    ChangeDetectorRef,
+    Component,
+    ElementRef,
+    Injector,
+    Input,
+    afterNextRender,
+    forwardRef,
+    inject
+} from '@angular/core';
 import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from 
'@angular/forms';
 import { MatButtonModule } from '@angular/material/button';
 import { MatDialogModule } from '@angular/material/dialog';
 import { MatTableDataSource, MatTableModule } from '@angular/material/table';
 import { NgTemplateOutlet } from '@angular/common';
-import { RouterLink } from '@angular/router';
 import { EditParameterResponse, ParameterEntity } from 
'../../../../../state/shared';
 import { NifiTooltipDirective, NiFiCommon, TextTip, Parameter } from 
'@nifi/shared';
 import { Observable, take } from 'rxjs';
@@ -51,7 +60,6 @@ export interface ParameterItem {
         MatTableModule,
         MatSortModule,
         NgTemplateOutlet,
-        RouterLink,
         NifiTooltipDirective,
         ParameterReferences,
         MatMenu,
@@ -74,11 +82,15 @@ export class ParameterTable implements AfterViewInit, 
ControlValueAccessor {
     private store = inject<Store<ParameterContextListingState>>(Store);
     private changeDetector = inject(ChangeDetectorRef);
     private nifiCommon = inject(NiFiCommon);
+    private elementRef = inject(ElementRef);
+    private injector = inject(Injector);
 
     @Input() createNewParameter!: (existingParameters: string[]) => 
Observable<EditParameterResponse>;
     @Input() editParameter!: (parameter: Parameter) => 
Observable<EditParameterResponse>;
+    @Input() goToParameter?: (parameterContextId: string, parameterName: 
string) => void;
     @Input() canAddParameters = true;
     @Input() inheritsParameters = false;
+    @Input() highlightedParameterName?: string;
 
     protected readonly TextTip = TextTip;
 
@@ -101,6 +113,24 @@ export class ParameterTable implements AfterViewInit, 
ControlValueAccessor {
 
     ngAfterViewInit(): void {
         this.initFilter();
+        if (this.highlightedParameterName) {
+            const match = this.dataSource.data.find(
+                (item) => this.isVisible(item) && 
item.originalEntity.parameter.name === this.highlightedParameterName
+            );
+            if (match) {
+                this.selectParameter(match);
+                afterNextRender(
+                    () => {
+                        const rows: NodeListOf<HTMLElement> =
+                            
this.elementRef.nativeElement.querySelectorAll('[data-parameter-name]');
+                        Array.from(rows)
+                            .find((el) => el.dataset['parameterName'] === 
this.highlightedParameterName)
+                            ?.scrollIntoView({ block: 'center', behavior: 
'smooth' });
+                    },
+                    { injector: this.injector }
+                );
+            }
+        }
     }
 
     initFilter(): void {
@@ -315,15 +345,19 @@ export class ParameterTable implements AfterViewInit, 
ControlValueAccessor {
     }
 
     canGoToParameter(item: ParameterItem): boolean {
-        return this.canOverride(item) && 
item.originalEntity.parameter.parameterContext?.permissions.canRead == true;
+        return (
+            !!this.goToParameter &&
+            this.canOverride(item) &&
+            
item.originalEntity.parameter.parameterContext?.permissions.canRead == true
+        );
     }
 
-    getParameterLink(item: ParameterItem): string[] {
-        if (item.originalEntity.parameter.parameterContext) {
-            // TODO - support routing directly to a parameter
-            return ['/parameter-contexts', 
item.originalEntity.parameter.parameterContext.id, 'edit'];
+    goToParameterClicked(item: ParameterItem): void {
+        const parameterContext = 
item.originalEntity.parameter.parameterContext;
+        if (!this.goToParameter || !parameterContext) {
+            return;
         }
-        return [];
+        this.goToParameter(parameterContext.id, 
item.originalEntity.parameter.name);
     }
 
     isOverridden(item: ParameterItem): boolean {
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 2472558dccb..e1fafc075bf 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
@@ -549,9 +549,16 @@ export interface ParameterConfig {
     parameters: Parameter[] | null;
 }
 
+export interface PostUpdateNavigationState {
+    highlightedParameterName?: string;
+}
+
 export interface SubmitParameterContextUpdate {
     id: string;
     payload: any;
+    postUpdateNavigation?: string[];
+    postUpdateNavigationBoundary?: string[];
+    postUpdateNavigationState?: PostUpdateNavigationState;
 }
 
 export interface PollParameterContextUpdateSuccess {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html
index 9ef15caaf6d..5e43ddedcd9 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html
@@ -157,7 +157,9 @@
                             
[canAddParameters]="!request.parameterContext?.component?.parameterProviderConfiguration"
                             
[inheritsParameters]="inheritsParameters(request.parameterContext?.component?.parameters)"
                             [createNewParameter]="createNewParameter"
-                            [editParameter]="editParameter"></parameter-table>
+                            [editParameter]="editParameter"
+                            [goToParameter]="goToParameter"
+                            
[highlightedParameterName]="request.highlightedParameterName"></parameter-table>
                     </div>
                 </mat-dialog-content>
             </mat-tab>
@@ -177,18 +179,46 @@
     @if ((updateRequest | async)!; as requestEntity) {
         <mat-dialog-actions align="end">
             @if (requestEntity.request.complete) {
-                <button mat-flat-button mat-dialog-close>Close</button>
+                @if ((hasPendingPostUpdateNavigation$ | async)!) {
+                    <button
+                        mat-button
+                        type="button"
+                        aria-label="Close"
+                        data-qa="edit-parameter-context-close"
+                        mat-dialog-close>
+                        Close
+                    </button>
+                    <button
+                        mat-flat-button
+                        type="button"
+                        aria-label="Go to Parameter"
+                        data-qa="edit-parameter-context-go-to-parameter"
+                        (click)="continuePostUpdateNavigation.emit()">
+                        Go to Parameter
+                    </button>
+                } @else {
+                    <button
+                        mat-flat-button
+                        type="button"
+                        aria-label="Close"
+                        data-qa="edit-parameter-context-close"
+                        mat-dialog-close>
+                        Close
+                    </button>
+                }
             } @else {
-                <button mat-button mat-dialog-close 
(click)="cancelUpdateRequest.emit()">Cancel</button>
+                <button mat-button type="button" mat-dialog-close 
(click)="cancelUpdateRequest.emit()">Cancel</button>
             }
         </mat-dialog-actions>
     } @else {
         @if ({ value: (saving$ | async)! }; as saving) {
             <mat-dialog-actions align="end">
                 @if (readonly) {
-                    <button mat-flat-button mat-dialog-close>Close</button>
+                    <button mat-flat-button type="button" 
mat-dialog-close>Close</button>
                 } @else {
-                    <button mat-button mat-dialog-close 
(click)="cancelUpdateRequest.emit()">Cancel</button>
+                    <button mat-button type="button" mat-dialog-close 
(click)="cancelUpdateRequest.emit()">
+                        Cancel
+                    </button>
                     <button
                         [disabled]="
                             !editParameterContextForm.dirty ||
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts
index 85c5751d766..fa5c533241d 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts
@@ -31,9 +31,10 @@ import { errorFeatureKey } from '../../../../state/error';
 import { initialState as initialCurrentUserState } from 
'../../../../state/current-user/current-user.reducer';
 import { currentUserFeatureKey } from '../../../../state/current-user';
 import { ClusterConnectionService } from 
'../../../../service/cluster-connection.service';
-import { ParameterContextEntity, ParameterEntity } from 
'../../../../state/shared';
+import { ParameterContextEntity, ParameterContextUpdateRequestEntity, 
ParameterEntity } from '../../../../state/shared';
 
-import { EditParameterContextRequest } from '../index';
+import { EditParameterContextRequest } from 
'../../../../pages/parameter-contexts/state/parameter-context-listing';
+import { By } from '@angular/platform-browser';
 
 describe('EditParameterContext', () => {
     let component: EditParameterContext;
@@ -268,6 +269,9 @@ describe('EditParameterContext', () => {
         fixture = TestBed.createComponent(EditParameterContext);
         component = fixture.componentInstance;
         component.availableParameterContexts$ = of(parameterContexts);
+        component.hasPendingPostUpdateNavigation$ = of(false);
+        component.updateRequest = of(null);
+        component.saving$ = of(false);
         fixture.detectChanges();
     });
 
@@ -280,6 +284,11 @@ describe('EditParameterContext', () => {
         expect(component.cancelUpdateRequest).toBeInstanceOf(EventEmitter);
     });
 
+    it('should have continuePostUpdateNavigation EventEmitter', () => {
+        expect(component.continuePostUpdateNavigation).toBeDefined();
+        
expect(component.continuePostUpdateNavigation).toBeInstanceOf(EventEmitter);
+    });
+
     it('should emit cancelUpdateRequest when called', () => {
         const spy = vi.spyOn(component.cancelUpdateRequest, 'emit');
 
@@ -288,6 +297,136 @@ describe('EditParameterContext', () => {
         expect(spy).toHaveBeenCalledTimes(1);
     });
 
+    describe('submitForm', () => {
+        it('should include postUpdateNavigation fields on editParameterContext 
emit', () => {
+            const spy = vi.spyOn(component.editParameterContext, 'next');
+            component.editParameterContextForm.markAsDirty();
+
+            component.submitForm(['/parameter-contexts', 'inherited-id', 
'edit'], ['/parameter-contexts'], {
+                highlightedParameterName: 'inherited-param'
+            });
+
+            expect(spy).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    payload: expect.objectContaining({
+                        id: data.parameterContext!.id
+                    }),
+                    postUpdateNavigation: ['/parameter-contexts', 
'inherited-id', 'edit'],
+                    postUpdateNavigationBoundary: ['/parameter-contexts'],
+                    postUpdateNavigationState: { highlightedParameterName: 
'inherited-param' }
+                })
+            );
+        });
+
+        it('should omit postUpdateNavigation fields when not provided', () => {
+            const spy = vi.spyOn(component.editParameterContext, 'next');
+            component.editParameterContextForm.markAsDirty();
+
+            component.submitForm();
+
+            expect(spy).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    payload: expect.objectContaining({
+                        id: data.parameterContext!.id
+                    }),
+                    postUpdateNavigation: undefined,
+                    postUpdateNavigationBoundary: undefined,
+                    postUpdateNavigationState: undefined
+                })
+            );
+        });
+    });
+
+    describe('post-update review actions', () => {
+        const completeUpdateRequest: ParameterContextUpdateRequestEntity = {
+            parameterContextRevision: { version: 1 },
+            request: {
+                complete: true,
+                lastUpdated: '2024-01-01T00:00:00.000Z',
+                percentComponent: 100,
+                referencingComponents: [],
+                requestId: 'request-1',
+                state: 'COMPLETE',
+                updateSteps: [],
+                uri: '/nifi-api/parameter-contexts/update-requests/request-1'
+            }
+        };
+
+        async function createReviewFixture(hasPendingNavigation: boolean): 
Promise<void> {
+            TestBed.resetTestingModule();
+            await TestBed.configureTestingModule({
+                imports: [EditParameterContext, NoopAnimationsModule],
+                providers: [
+                    { provide: MAT_DIALOG_DATA, useValue: data },
+                    provideMockStore({
+                        initialState: {
+                            [errorFeatureKey]: initialErrorState,
+                            [currentUserFeatureKey]: initialCurrentUserState,
+                            [parameterContextsFeatureKey]: {
+                                [parameterContextListingFeatureKey]: 
initialState
+                            }
+                        }
+                    }),
+                    {
+                        provide: ClusterConnectionService,
+                        useValue: {
+                            isDisconnectionAcknowledged: vi.fn()
+                        }
+                    },
+                    { provide: MatDialogRef, useValue: null }
+                ]
+            }).compileComponents();
+
+            fixture = TestBed.createComponent(EditParameterContext);
+            component = fixture.componentInstance;
+            component.availableParameterContexts$ = of(parameterContexts);
+            component.hasPendingPostUpdateNavigation$ = 
of(hasPendingNavigation);
+            component.updateRequest = of(completeUpdateRequest);
+            component.saving$ = of(false);
+            fixture.detectChanges();
+        }
+
+        it('should show Close as secondary and Go to Parameter as primary when 
navigation is pending', async () => {
+            await createReviewFixture(true);
+
+            const closeButton = 
fixture.debugElement.query(By.css('button[data-qa="edit-parameter-context-close"]'));
+            const goToButton = fixture.debugElement.query(
+                
By.css('button[data-qa="edit-parameter-context-go-to-parameter"]')
+            );
+
+            expect(closeButton).toBeTruthy();
+            expect(closeButton.attributes['mat-button']).toBeDefined();
+            expect(goToButton).toBeTruthy();
+            expect(goToButton.attributes['mat-flat-button']).toBeDefined();
+        });
+
+        it('should emit continuePostUpdateNavigation when Go to Parameter is 
clicked', async () => {
+            await createReviewFixture(true);
+
+            const emitSpy = vi.spyOn(component.continuePostUpdateNavigation, 
'emit');
+            const goToButton = fixture.debugElement.query(
+                
By.css('button[data-qa="edit-parameter-context-go-to-parameter"]')
+            );
+
+            goToButton.nativeElement.click();
+
+            expect(emitSpy).toHaveBeenCalledTimes(1);
+        });
+
+        it('should show only Close as primary when no post-update navigation 
is pending', async () => {
+            await createReviewFixture(false);
+
+            const closeButton = 
fixture.debugElement.query(By.css('button[data-qa="edit-parameter-context-close"]'));
+            const goToButton = fixture.debugElement.query(
+                
By.css('button[data-qa="edit-parameter-context-go-to-parameter"]')
+            );
+
+            expect(closeButton).toBeTruthy();
+            expect(closeButton.attributes['mat-flat-button']).toBeDefined();
+            expect(goToButton).toBeNull();
+        });
+    });
+
     describe('inheritsParameters', () => {
         it('should return true if parameters are inherited', () => {
             const parameters: ParameterEntity[] = [
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts
index 59e3e75279d..b2d8e1f647c 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts
@@ -25,7 +25,7 @@ import { AsyncPipe } from '@angular/common';
 import { MatTabsModule } from '@angular/material/tabs';
 import { MatOptionModule } from '@angular/material/core';
 import { MatSelectModule } from '@angular/material/select';
-import { Observable } from 'rxjs';
+import { Observable, of } from 'rxjs';
 import { EditParameterContextRequest } from 
'../../../../pages/parameter-contexts/state/parameter-context-listing';
 import { Client } from '../../../../service/client.service';
 import { ParameterTable } from 
'../../../../pages/parameter-contexts/ui/parameter-context-listing/parameter-table/parameter-table.component';
@@ -35,8 +35,10 @@ import {
     ParameterContextEntity,
     ParameterContextUpdateRequestEntity,
     ParameterEntity,
-    ParameterProviderConfiguration
+    ParameterProviderConfiguration,
+    PostUpdateNavigationState
 } from '../../../../state/shared';
+import { EditParameterContextUpdate } from '../index';
 import { ProcessGroupReferences } from 
'../../process-group-references/process-group-references.component';
 import { ParameterContextInheritance } from 
'../parameter-context-inheritance/parameter-context-inheritance.component';
 import { ParameterReferences } from 
'../../parameter-references/parameter-references.component';
@@ -93,13 +95,17 @@ export class EditParameterContext extends TabbedDialog {
 
     @Input() createNewParameter!: (existingParameters: string[]) => 
Observable<EditParameterResponse>;
     @Input() editParameter!: (parameter: Parameter) => 
Observable<EditParameterResponse>;
+    @Input() goToParameter?: (parameterContextId: string, parameterName: 
string) => void;
     @Input() updateRequest!: Observable<ParameterContextUpdateRequestEntity | 
null>;
     @Input() availableParameterContexts$!: 
Observable<ParameterContextEntity[]>;
     @Input() saving$!: Observable<boolean>;
+    @Input() hasPendingPostUpdateNavigation$: Observable<boolean> = of(false);
 
     @Output() addParameterContext: EventEmitter<any> = new EventEmitter<any>();
-    @Output() editParameterContext: EventEmitter<any> = new 
EventEmitter<any>();
-    @Output() cancelUpdateRequest: EventEmitter<any> = new EventEmitter<any>();
+    @Output() editParameterContext: EventEmitter<EditParameterContextUpdate> =
+        new EventEmitter<EditParameterContextUpdate>();
+    @Output() cancelUpdateRequest: EventEmitter<void> = new 
EventEmitter<void>();
+    @Output() continuePostUpdateNavigation: EventEmitter<void> = new 
EventEmitter<void>();
 
     editParameterContextForm: FormGroup;
     readonly: boolean;
@@ -165,7 +171,11 @@ export class EditParameterContext extends TabbedDialog {
         return false;
     }
 
-    submitForm() {
+    submitForm(
+        postUpdateNavigation?: string[],
+        postUpdateNavigationBoundary?: string[],
+        postUpdateNavigationState?: PostUpdateNavigationState
+    ) {
         if (this.isNew) {
             const payload: any = {
                 revision: {
@@ -215,7 +225,12 @@ export class EditParameterContext extends TabbedDialog {
                 }
             };
 
-            this.editParameterContext.next(payload);
+            this.editParameterContext.next({
+                payload,
+                postUpdateNavigation,
+                postUpdateNavigationBoundary,
+                postUpdateNavigationState
+            });
         }
     }
 
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/index.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/index.ts
index 4d4847b7795..24924274725 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/index.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/index.ts
@@ -15,10 +15,13 @@
  * limitations under the License.
  */
 
-import { ParameterContextEntity } from '../../../state/shared';
+import { ParameterContextEntity, PostUpdateNavigationState } from 
'../../../state/shared';
 
-export interface EditParameterContextRequest {
-    parameterContext?: ParameterContextEntity;
+export interface EditParameterContextUpdate {
+    payload: any;
+    postUpdateNavigation?: string[];
+    postUpdateNavigationBoundary?: string[];
+    postUpdateNavigationState?: PostUpdateNavigationState;
 }
 
 export interface CreateParameterContextRequest {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.html
new file mode 100644
index 00000000000..c46c61e291c
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.html
@@ -0,0 +1,82 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title data-qa="save-parameter-context-changes-title">Unsaved 
Changes</h2>
+<mat-dialog-content>
+    @if (request.canSave) {
+        <div class="text-base" 
data-qa="save-parameter-context-changes-message">
+            This Parameter Context has unsaved changes. Save them before going 
to this {{ request.destination }}?
+            Choosing "Don't Save" will discard all pending changes.
+        </div>
+    } @else {
+        <div class="text-base" 
data-qa="save-parameter-context-changes-message">
+            This Parameter Context has unsaved changes, but they can't be 
saved because the form is not in a saveable
+            state. Choose "Don't Save" to discard all pending changes and 
continue, or "Cancel" to keep editing.
+        </div>
+    }
+</mat-dialog-content>
+<mat-dialog-actions align="end">
+    <button
+        mat-button
+        type="button"
+        mat-dialog-close
+        aria-label="Cancel"
+        data-qa="save-parameter-context-changes-cancel-button">
+        Cancel
+    </button>
+    @if (request.canSave) {
+        <button
+            mat-button
+            type="button"
+            mat-dialog-close
+            aria-label="Don't Save"
+            (click)="discardClicked()"
+            data-qa="save-parameter-context-changes-discard-button">
+            Don't Save
+        </button>
+        <button
+            mat-flat-button
+            cdkFocusInitial
+            type="button"
+            mat-dialog-close
+            aria-label="Save"
+            (click)="saveClicked()"
+            data-qa="save-parameter-context-changes-save-button">
+            Save
+        </button>
+    } @else {
+        <button
+            mat-button
+            cdkFocusInitial
+            type="button"
+            mat-dialog-close
+            aria-label="Don't Save"
+            (click)="discardClicked()"
+            data-qa="save-parameter-context-changes-discard-button">
+            Don't Save
+        </button>
+        <button
+            mat-flat-button
+            type="button"
+            mat-dialog-close
+            disabled
+            aria-label="Save"
+            data-qa="save-parameter-context-changes-save-button">
+            Save
+        </button>
+    }
+</mat-dialog-actions>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.spec.ts
new file mode 100644
index 00000000000..4d6842837e3
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { By } from '@angular/platform-browser';
+
+import {
+    SaveParameterContextChangesDialog,
+    SaveParameterContextChangesDialogRequest
+} from './save-parameter-context-changes-dialog.component';
+
+describe('SaveParameterContextChangesDialog', () => {
+    let component: SaveParameterContextChangesDialog;
+    let fixture: ComponentFixture<SaveParameterContextChangesDialog>;
+
+    function setup(data: SaveParameterContextChangesDialogRequest) {
+        TestBed.configureTestingModule({
+            imports: [SaveParameterContextChangesDialog],
+            providers: [
+                { provide: MAT_DIALOG_DATA, useValue: data },
+                { provide: MatDialogRef, useValue: null }
+            ]
+        });
+        fixture = TestBed.createComponent(SaveParameterContextChangesDialog);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    }
+
+    afterEach(() => {
+        TestBed.resetTestingModule();
+    });
+
+    it('should create', () => {
+        setup({ destination: 'Parameter', canSave: true });
+        expect(component).toBeTruthy();
+    });
+
+    it('should emit when save clicked', () => {
+        setup({ destination: 'Parameter', canSave: true });
+        const emitSpy = vi.spyOn(component.save, 'next');
+        component.saveClicked();
+        expect(emitSpy).toHaveBeenCalled();
+    });
+
+    it('should emit when discard clicked', () => {
+        setup({ destination: 'Parameter', canSave: true });
+        const emitSpy = vi.spyOn(component.discard, 'next');
+        component.discardClicked();
+        expect(emitSpy).toHaveBeenCalled();
+    });
+
+    it('should enable Save, focus Save, and mention discarding pending changes 
when the form can be saved', () => {
+        setup({ destination: 'Parameter', canSave: true });
+
+        const saveButton = fixture.debugElement.query(
+            
By.css('button[data-qa="save-parameter-context-changes-save-button"]')
+        );
+        const discardButton = fixture.debugElement.query(
+            
By.css('button[data-qa="save-parameter-context-changes-discard-button"]')
+        );
+        const message = 
fixture.debugElement.query(By.css('div[data-qa="save-parameter-context-changes-message"]'));
+
+        expect(saveButton.nativeElement.disabled).toBe(false);
+        expect(saveButton.attributes['cdkFocusInitial']).toBeDefined();
+        expect(discardButton.attributes['cdkFocusInitial']).toBeUndefined();
+        expect(message.nativeElement.textContent).toContain('discard all 
pending changes');
+    });
+
+    it("should disable Save, focus Don't Save, and indicate the form is not 
saveable when the form cannot be saved", () => {
+        setup({ destination: 'Parameter', canSave: false });
+
+        const saveButton = fixture.debugElement.query(
+            
By.css('button[data-qa="save-parameter-context-changes-save-button"]')
+        );
+        const discardButton = fixture.debugElement.query(
+            
By.css('button[data-qa="save-parameter-context-changes-discard-button"]')
+        );
+        const message = 
fixture.debugElement.query(By.css('div[data-qa="save-parameter-context-changes-message"]'));
+
+        expect(saveButton.nativeElement.disabled).toBe(true);
+        expect(discardButton.attributes['cdkFocusInitial']).toBeDefined();
+        expect(saveButton.attributes['cdkFocusInitial']).toBeUndefined();
+        expect(message.nativeElement.textContent).toContain('not in a 
saveable');
+    });
+
+    it('should always render a Cancel button that only closes the dialog', () 
=> {
+        setup({ destination: 'Parameter', canSave: false });
+
+        const cancelButton = fixture.debugElement.query(
+            
By.css('button[data-qa="save-parameter-context-changes-cancel-button"]')
+        );
+        expect(cancelButton).toBeTruthy();
+        expect(cancelButton.attributes['mat-dialog-close']).toBeDefined();
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.ts
new file mode 100644
index 00000000000..08c587fe000
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/save-parameter-context-changes-dialog/save-parameter-context-changes-dialog.component.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { Component, EventEmitter, Output, inject } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatButtonModule } from '@angular/material/button';
+import { CloseOnEscapeDialog } from '@nifi/shared';
+
+export interface SaveParameterContextChangesDialogRequest {
+    destination: string;
+    canSave: boolean;
+}
+
+@Component({
+    selector: 'save-parameter-context-changes-dialog',
+    imports: [MatDialogModule, MatButtonModule],
+    templateUrl: './save-parameter-context-changes-dialog.component.html'
+})
+export class SaveParameterContextChangesDialog extends CloseOnEscapeDialog {
+    request = 
inject<SaveParameterContextChangesDialogRequest>(MAT_DIALOG_DATA);
+
+    @Output() save: EventEmitter<void> = new EventEmitter<void>();
+    @Output() discard: EventEmitter<void> = new EventEmitter<void>();
+
+    saveClicked(): void {
+        this.save.next();
+    }
+
+    discardClicked(): void {
+        this.discard.next();
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/nx.json 
b/nifi-frontend/src/main/frontend/nx.json
index f20867cbbc9..6a36ce76177 100644
--- a/nifi-frontend/src/main/frontend/nx.json
+++ b/nifi-frontend/src/main/frontend/nx.json
@@ -121,5 +121,6 @@
         "@schematics/angular:resolver": {
             "typeSeparator": "."
         }
-    }
+    },
+    "analytics": false
 }

Reply via email to