From 180e16dcdff1e7c8230293269ceaef3708711f0a Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 8 Jul 2026 08:57:59 +0200 Subject: [PATCH 1/8] fix: enhance routing for CVE and business value management with deep linking support --- src/main/frontend/src/app/app.routes.ts | 7 ++++ .../cve-overview-left-panel.component.html | 1 + .../cve-overview-right-panel.component.html | 3 +- ...cve-overview-right-panel.component.spec.ts | 2 +- .../cve-overview/cve-overview.component.ts | 38 +++++++++++++++++-- .../business-value-manage.component.spec.ts | 4 +- .../business-value-manage.component.ts | 23 ++++++++++- ...nerability-impact-manage.component.spec.ts | 12 +++--- .../vulnerability-impact-manage.component.ts | 4 +- 9 files changed, 77 insertions(+), 17 deletions(-) diff --git a/src/main/frontend/src/app/app.routes.ts b/src/main/frontend/src/app/app.routes.ts index 3813e21f..3dcf6b75 100644 --- a/src/main/frontend/src/app/app.routes.ts +++ b/src/main/frontend/src/app/app.routes.ts @@ -20,17 +20,24 @@ export const routes: Routes = [ }, { path: 'roadmap', component: ReleaseRoadmapComponent }, { path: 'cve-overview', component: CveOverviewComponent }, + { path: 'cve-overview/:cveId', component: CveOverviewComponent }, { path: 'vulnerabilities/manage', component: VulnerabilityImpactManageComponent, canActivate: [FrankFrameworkMemberGuard], }, + { + path: 'vulnerabilities/manage/:cveId', + component: VulnerabilityImpactManageComponent, + canActivate: [FrankFrameworkMemberGuard], + }, { path: 'release-manage/:id', canActivate: [FrankFrameworkMemberGuard], children: [ { path: '', component: ReleaseManageComponent }, { path: 'business-values', component: BusinessValueManageComponent }, + { path: 'business-values/:businessValueId', component: BusinessValueManageComponent }, ], }, { path: 'not-found', component: NotFoundComponent }, diff --git a/src/main/frontend/src/app/pages/cve-overview/cve-overview-left-panel/cve-overview-left-panel.component.html b/src/main/frontend/src/app/pages/cve-overview/cve-overview-left-panel/cve-overview-left-panel.component.html index 7195fdd8..2403210f 100644 --- a/src/main/frontend/src/app/pages/cve-overview/cve-overview-left-panel/cve-overview-left-panel.component.html +++ b/src/main/frontend/src/app/pages/cve-overview/cve-overview-left-panel/cve-overview-left-panel.component.html @@ -56,6 +56,7 @@ @for (detail of vulnerabilityDetails; track detail.vulnerability.cveId) { Manage Impact diff --git a/src/main/frontend/src/app/pages/cve-overview/cve-overview-right-panel/cve-overview-right-panel.component.spec.ts b/src/main/frontend/src/app/pages/cve-overview/cve-overview-right-panel/cve-overview-right-panel.component.spec.ts index 931858c1..1899be55 100644 --- a/src/main/frontend/src/app/pages/cve-overview/cve-overview-right-panel/cve-overview-right-panel.component.spec.ts +++ b/src/main/frontend/src/app/pages/cve-overview/cve-overview-right-panel/cve-overview-right-panel.component.spec.ts @@ -113,7 +113,7 @@ describe('CveOverviewRightPanelComponent', () => { expect(manageButton).toBeTruthy(); expect(manageButton.getAttribute('href')).toContain('/vulnerabilities/manage'); - expect(manageButton.getAttribute('href')).toContain('cve=CVE-2024-0001'); + expect(manageButton.getAttribute('href')).toContain('/CVE-2024-0001'); }); }); diff --git a/src/main/frontend/src/app/pages/cve-overview/cve-overview.component.ts b/src/main/frontend/src/app/pages/cve-overview/cve-overview.component.ts index a03fd757..1f0e4458 100644 --- a/src/main/frontend/src/app/pages/cve-overview/cve-overview.component.ts +++ b/src/main/frontend/src/app/pages/cve-overview/cve-overview.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, OnDestroy, inject, signal, computed } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; import { Observable, Subject, catchError, debounceTime, distinctUntilChanged, finalize, of, takeUntil } from 'rxjs'; import { Vulnerability, @@ -94,11 +95,14 @@ export class CveOverviewComponent implements OnInit, OnDestroy { private readonly releaseService = inject(ReleaseService); private readonly authService = inject(AuthService); private readonly impactLabelPipe = inject(ImpactLabelPipe); + private readonly location = inject(Location); + private readonly route = inject(ActivatedRoute); private readonly destroy$ = new Subject(); private readonly searchSubject = new Subject(); private readonly branchStartDates = signal>(new Map()); private isBulkLoading = false; + private pendingCveId: string | null = null; private static emptyPage(page: number): VulnerabilityPage { return { @@ -175,8 +179,11 @@ export class CveOverviewComponent implements OnInit, OnDestroy { this.resetAndFetch(); }); + this.pendingCveId = this.route.snapshot.paramMap.get('cveId'); this.loadBranchStartDates(); - this.fetchPage(0); + this.fetchPage(0, () => { + if (this.pendingCveId) this.findAndSelectCve(this.pendingCveId); + }); } ngOnDestroy(): void { @@ -187,6 +194,7 @@ export class CveOverviewComponent implements OnInit, OnDestroy { public onSelectCve(vulnerability: Vulnerability): void { this.selectedCveId.set(vulnerability.cveId); this.showFilterPanel.set(false); + this.location.go(`/cve-overview/${vulnerability.cveId}`); } public onSearch(query: string): void { @@ -208,11 +216,16 @@ export class CveOverviewComponent implements OnInit, OnDestroy { this.showFilterPanel.update((isVisible) => !isVisible); } - public fetchPage(page: number): void { + public fetchPage(page: number, onComplete?: () => void): void { if (this.isLoading()) return; this.isLoading.set(true); this.requestPage(page) - .pipe(finalize(() => this.isLoading.set(false))) + .pipe( + finalize(() => { + this.isLoading.set(false); + onComplete?.(); + }), + ) .subscribe((data) => { this.allDetails.update((existing) => (page === 0 ? data.content : [...existing, ...data.content])); this.currentPage.set(data.number); @@ -234,6 +247,23 @@ export class CveOverviewComponent implements OnInit, OnDestroy { this.fetchPage(0); } + private findAndSelectCve(cveId: string): void { + const match = this.allDetails().find((detail) => detail.vulnerability.cveId === cveId); + + if (match) { + this.selectedCveId.set(cveId); + this.pendingCveId = null; + setTimeout(() => { + document.querySelector(`#cve-item-${cveId}`)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + }, 100); + return; + } + + if (this.isLastPage()) return; + + this.fetchPage(this.currentPage() + 1, () => this.findAndSelectCve(cveId)); + } + private loadBranchStartDates(): void { this.releaseService .getAllReleases() diff --git a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts index eb9abb20..28cdc569 100644 --- a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts +++ b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts @@ -79,7 +79,7 @@ describe('BusinessValueManageComponent', () => { ]); mockIssueService = jasmine.createSpyObj('IssueService', ['getIssuesByReleaseId']); mockReleaseService = jasmine.createSpyObj('ReleaseService', ['getReleaseById', 'getAllReleases']); - mockLocation = jasmine.createSpyObj('Location', ['back']); + mockLocation = jasmine.createSpyObj('Location', ['back', 'go']); mockBusinessValueService.getBusinessValuesByReleaseId.and.returnValue(of(mockBusinessValues)); mockIssueService.getIssuesByReleaseId.and.returnValue(of(mockIssues)); @@ -96,7 +96,7 @@ describe('BusinessValueManageComponent', () => { { provide: ActivatedRoute, useValue: { - snapshot: { paramMap: { get: () => 'release-123' } }, + snapshot: { paramMap: { get: (key: string) => key === 'id' ? 'release-123' : null } }, }, }, provideHttpClient(), diff --git a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts index 83fe984a..6d2fc250 100644 --- a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts @@ -62,11 +62,13 @@ export class BusinessValueManageComponent implements OnInit { private releaseService = inject(ReleaseService); private originalSelectedIssueIds = signal>(new Set()); + private pendingBusinessValueId: string | null = null; ngOnInit(): void { const releaseId = this.route.snapshot.paramMap.get('id'); if (releaseId) { this.releaseId.set(releaseId); + this.pendingBusinessValueId = this.route.snapshot.paramMap.get('businessValueId'); this.fetchData(releaseId); } } @@ -126,6 +128,7 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === deletedId) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); + this.location.go(`/release-manage/${this.releaseId()}/business-values`); } this.closeDeleteModal(); @@ -149,8 +152,10 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === businessValue.id) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); + this.location.go(`/release-manage/${this.releaseId()}/business-values`); } else { this.selectedBusinessValue.set(businessValue); + this.location.go(`/release-manage/${this.releaseId()}/business-values/${businessValue.id}`); this.businessValueService.getBusinessValueById(businessValue.id).subscribe({ next: (detailedBV) => { const updatedList = this.businessValues().map((bv) => (bv.id === detailedBV.id ? detailedBV : bv)); @@ -239,7 +244,12 @@ export class BusinessValueManageComponent implements OnInit { release: this.releaseService.getReleaseById(releaseId).pipe(catchError(() => of(null))), allReleases: this.releaseService.getAllReleases().pipe(catchError(() => of([]))), }) - .pipe(finalize(() => this.isLoading.set(false))) + .pipe( + finalize(() => { + this.isLoading.set(false); + this.applyDeepLinkedBusinessValue(); + }), + ) .subscribe(({ businessValues, issues, release, allReleases }) => { this.businessValues.set(businessValues); this.allIssues.set(issues ?? []); @@ -265,6 +275,17 @@ export class BusinessValueManageComponent implements OnInit { }); } + private applyDeepLinkedBusinessValue(): void { + if (!this.pendingBusinessValueId) return; + + const match = this.businessValues().find((businessValue) => businessValue.id === this.pendingBusinessValueId); + + if (match) { + this.pendingBusinessValueId = null; + this.selectBusinessValue(match); + } + } + private updateIssueSelection(currentBusinessValue: BusinessValue): void { const currentConnectedIds = new Set(currentBusinessValue.issues?.map((issue) => issue.id)); diff --git a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts index 5f66c0bf..ae111194 100644 --- a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts +++ b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts @@ -60,9 +60,9 @@ describe('VulnerabilityImpactManageComponent', () => { 'deleteVulnerabilityImpact' ]); const releaseServiceSpy = jasmine.createSpyObj('ReleaseService', ['getAllReleases']); - const locationSpy = jasmine.createSpyObj('Location', ['back']); + const locationSpy = jasmine.createSpyObj('Location', ['back', 'go']); queryParameterGet = jasmine.createSpy('get').and.returnValue(null); - const activatedRouteStub = { snapshot: { queryParamMap: { get: queryParameterGet } } }; + const activatedRouteStub = { snapshot: { paramMap: { get: queryParameterGet } } }; await TestBed.configureTestingModule({ imports: [VulnerabilityImpactManageComponent], @@ -207,16 +207,16 @@ describe('VulnerabilityImpactManageComponent', () => { expect(location.back).toHaveBeenCalledWith(); }); - it('should pre-select a vulnerability from the cve query parameter', () => { + it('should pre-select a vulnerability from the cve path parameter', () => { queryParameterGet.and.returnValue('CVE-2024-0002'); fixture.detectChanges(); - expect(queryParameterGet).toHaveBeenCalledWith('cve'); + expect(queryParameterGet).toHaveBeenCalledWith('cveId'); expect(component.selectedVulnerability()?.cveId).toBe('CVE-2024-0002'); }); - it('should ignore an unknown cve query parameter', () => { + it('should ignore an unknown cve path parameter', () => { queryParameterGet.and.returnValue('CVE-9999-9999'); fixture.detectChanges(); @@ -224,7 +224,7 @@ describe('VulnerabilityImpactManageComponent', () => { expect(component.selectedVulnerability()).toBeNull(); }); - it('should not pre-select a vulnerability when there is no cve query parameter', () => { + it('should not pre-select a vulnerability when there is no cve path parameter', () => { fixture.detectChanges(); expect(component.selectedVulnerability()).toBeNull(); diff --git a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts index 045fbb0f..ae13dd1f 100644 --- a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts @@ -218,8 +218,10 @@ export class VulnerabilityImpactManageComponent implements OnInit, OnDestroy { public onVulnerabilitySelected(vulnerability: Vulnerability): void { if (this.selectedVulnerability()?.cveId === vulnerability.cveId) { this.selectedVulnerability.set(null); + this.location.go('/vulnerabilities/manage'); } else { this.selectedVulnerability.set(vulnerability); + this.location.go(`/vulnerabilities/manage/${vulnerability.cveId}`); } } @@ -362,7 +364,7 @@ export class VulnerabilityImpactManageComponent implements OnInit, OnDestroy { } private applyDeepLinkedCve(): void { - const cveId = this.route.snapshot.queryParamMap.get('cve'); + const cveId = this.route.snapshot.paramMap.get('cveId'); if (!cveId) return; this.findAndScrollToCve(cveId); } From 827abbecc667f8fc2c3da354d71155ecd83ab1e5 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Mon, 20 Jul 2026 08:50:16 +0200 Subject: [PATCH 2/8] fix: update routing to use Router for navigation in business value management --- .../business-value-manage.component.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts index 6d2fc250..68fd34e3 100644 --- a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, inject, signal, computed } from '@angular/core'; import { CommonModule, Location } from '@angular/common'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { BusinessValue, BusinessValueService } from '../../../services/business-value.service'; import { Issue, IssueService } from '../../../services/issue.service'; import { Release, ReleaseService } from '../../../services/release.service'; @@ -60,6 +60,7 @@ export class BusinessValueManageComponent implements OnInit { private businessValueService = inject(BusinessValueService); private issueService = inject(IssueService); private releaseService = inject(ReleaseService); + private router = inject(Router); private originalSelectedIssueIds = signal>(new Set()); private pendingBusinessValueId: string | null = null; @@ -128,7 +129,7 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === deletedId) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); - this.location.go(`/release-manage/${this.releaseId()}/business-values`); + this.router.navigate([`/release-manage/${this.releaseId()}/business-values`]); } this.closeDeleteModal(); @@ -152,10 +153,10 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === businessValue.id) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); - this.location.go(`/release-manage/${this.releaseId()}/business-values`); + this.router.navigate([`/release-manage/${this.releaseId()}/business-values`]); } else { this.selectedBusinessValue.set(businessValue); - this.location.go(`/release-manage/${this.releaseId()}/business-values/${businessValue.id}`); + this.router.navigate([`/release-manage/${this.releaseId()}/business-values/${businessValue.id}`]); this.businessValueService.getBusinessValueById(businessValue.id).subscribe({ next: (detailedBV) => { const updatedList = this.businessValues().map((bv) => (bv.id === detailedBV.id ? detailedBV : bv)); From 9b0d3235c9388900cba0ecf7fda67969cda8ca45 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Mon, 20 Jul 2026 11:53:28 +0200 Subject: [PATCH 3/8] fix: improve routing for business value management with clearer path structure --- .../business-value-manage.component.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts index 68fd34e3..effb0e53 100644 --- a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts @@ -129,7 +129,7 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === deletedId) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); - this.router.navigate([`/release-manage/${this.releaseId()}/business-values`]); + this.router.navigate(['/release-manage', this.releaseId(), 'business-values']); } this.closeDeleteModal(); @@ -153,10 +153,11 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === businessValue.id) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); - this.router.navigate([`/release-manage/${this.releaseId()}/business-values`]); + this.router.navigate(['/release-manage', this.releaseId(), 'business-values']); } else { this.selectedBusinessValue.set(businessValue); - this.router.navigate([`/release-manage/${this.releaseId()}/business-values/${businessValue.id}`]); + this.router.navigate(['/release-manage', this.releaseId(), 'business-values', businessValue.id]); + this.businessValueService.getBusinessValueById(businessValue.id).subscribe({ next: (detailedBV) => { const updatedList = this.businessValues().map((bv) => (bv.id === detailedBV.id ? detailedBV : bv)); From b5b306ca4918bfdeb94cbe687c231bd0d4bc1085 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Mon, 20 Jul 2026 14:18:59 +0200 Subject: [PATCH 4/8] fix: refactor routing in business value and vulnerability impact management for improved navigation --- .../business-value-manage.component.ts | 18 ++++++++++++------ .../release-manage/release-manage.component.ts | 13 ++++++------- .../vulnerability-impact-manage.component.ts | 15 ++++++++++++--- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts index effb0e53..2b824d21 100644 --- a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, inject, signal, computed } from '@angular/core'; -import { CommonModule, Location } from '@angular/common'; +import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { BusinessValue, BusinessValueService } from '../../../services/business-value.service'; import { Issue, IssueService } from '../../../services/issue.service'; @@ -15,6 +15,7 @@ import { IssueWithSelection, } from './business-value-issue-panel/business-value-issue-panel.component'; import { LoaderComponent } from '../../../components/loader/loader.component'; +import { ReleaseManageComponent } from '../release-manage.component'; @Component({ selector: 'app-business-value-manage', @@ -56,7 +57,6 @@ export class BusinessValueManageComponent implements OnInit { public hasChanges = computed(() => this.hasIssueChanges()); private route = inject(ActivatedRoute); - private location = inject(Location); private businessValueService = inject(BusinessValueService); private issueService = inject(IssueService); private releaseService = inject(ReleaseService); @@ -64,6 +64,7 @@ export class BusinessValueManageComponent implements OnInit { private originalSelectedIssueIds = signal>(new Set()); private pendingBusinessValueId: string | null = null; + private readonly businessValuesPath = 'business-values'; ngOnInit(): void { const releaseId = this.route.snapshot.paramMap.get('id'); @@ -75,7 +76,7 @@ export class BusinessValueManageComponent implements OnInit { } public goBack(): void { - this.location.back(); + this.router.navigate([ReleaseManageComponent.releaseManagePath, this.releaseId()]); } public toggleCreateForm(): void { @@ -129,7 +130,7 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === deletedId) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); - this.router.navigate(['/release-manage', this.releaseId(), 'business-values']); + this.router.navigate([ReleaseManageComponent.releaseManagePath, this.releaseId(), this.businessValuesPath]); } this.closeDeleteModal(); @@ -153,10 +154,15 @@ export class BusinessValueManageComponent implements OnInit { if (this.selectedBusinessValue()?.id === businessValue.id) { this.selectedBusinessValue.set(null); this.resetIssueSelection(); - this.router.navigate(['/release-manage', this.releaseId(), 'business-values']); + this.router.navigate([ReleaseManageComponent.releaseManagePath, this.releaseId(), this.businessValuesPath]); } else { this.selectedBusinessValue.set(businessValue); - this.router.navigate(['/release-manage', this.releaseId(), 'business-values', businessValue.id]); + this.router.navigate([ + ReleaseManageComponent.releaseManagePath, + this.releaseId(), + this.businessValuesPath, + businessValue.id, + ]); this.businessValueService.getBusinessValueById(businessValue.id).subscribe({ next: (detailedBV) => { diff --git a/src/main/frontend/src/app/pages/release-manage/release-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/release-manage.component.ts index 65452e08..3d36a4f3 100644 --- a/src/main/frontend/src/app/pages/release-manage/release-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/release-manage.component.ts @@ -19,6 +19,8 @@ import { BusinessValue, BusinessValueService } from '../../services/business-val styleUrl: './release-manage.component.scss', }) export class ReleaseManageComponent implements OnInit { + public static readonly releaseManagePath = '/release-manage'; + public authService = inject(AuthService); public release = signal(null); public releaseIssues = signal([]); @@ -51,16 +53,13 @@ export class ReleaseManageComponent implements OnInit { } public openSection(section: 'business-value' | 'vulnerabilities'): void { - if (section === 'vulnerabilities') { - this.router.navigate(['/vulnerabilities/manage']); - return; - } - const releaseId = this.release()?.id; if (!releaseId) return; - if (section === 'business-value') { - this.router.navigate(['/release-manage', releaseId, 'business-values']); + if (section === 'vulnerabilities') { + this.router.navigate(['/vulnerabilities/manage'], { queryParams: { releaseId: releaseId } }); + } else if (section === 'business-value') { + this.router.navigate([ReleaseManageComponent.releaseManagePath, releaseId, 'business-values']); } } diff --git a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts index ae13dd1f..20a7468a 100644 --- a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, OnDestroy, inject, signal, computed } from '@angular/core'; +import { Component, OnInit, OnDestroy, inject, input, signal, computed, InputSignal } from '@angular/core'; import { CommonModule, Location } from '@angular/common'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Vulnerability, VulnerabilityDetail, @@ -16,6 +16,7 @@ import { ImpactLabelPipe } from '../../../pipes/impact-label.pipe'; import { sortVersionsAsc } from '../../../pipes/version-compare'; import { isBranchMaintained } from '../../../pipes/branch-lifecycle'; import { Release, ReleaseService } from '../../../services/release.service'; +import { ReleaseManageComponent } from '../release-manage.component'; @Component({ selector: 'app-vulnerability-impact-manage', @@ -41,6 +42,7 @@ export class VulnerabilityImpactManageComponent implements OnInit, OnDestroy { public vulnerabilitySearchQuery = signal(''); public showFilterPanel = signal(false); public filters = signal({ ...DEFAULT_CVE_FILTERS }); + public releaseId: InputSignal = input(null); public readonly branchMaintainedMap = computed>(() => { const startDates = this.branchStartDates(); @@ -133,6 +135,8 @@ export class VulnerabilityImpactManageComponent implements OnInit, OnDestroy { private location = inject(Location); private route = inject(ActivatedRoute); private impactLabelPipe = inject(ImpactLabelPipe); + private router = inject(Router); + private readonly destroy$ = new Subject(); private readonly searchSubject = new Subject(); private isLoadingAll = false; @@ -187,7 +191,12 @@ export class VulnerabilityImpactManageComponent implements OnInit, OnDestroy { } public goBack(): void { - this.location.back(); + const releaseId = this.releaseId(); + if (releaseId) { + this.router.navigate([ReleaseManageComponent.releaseManagePath, releaseId]); + } else { + this.location.back(); + } } public onSearchQueryChanged(query: string): void { From 2273816f6f802ba79b8f0187f123a662b47e44ab Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Mon, 20 Jul 2026 14:32:49 +0200 Subject: [PATCH 5/8] fix: enhance vulnerabilities link with query parameters for improved navigation --- .../release-manage.component.html | 6 +- .../release-manage.component.ts | 82 +++++++++---------- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/main/frontend/src/app/pages/release-manage/release-manage.component.html b/src/main/frontend/src/app/pages/release-manage/release-manage.component.html index a227eebc..3e49b8cb 100644 --- a/src/main/frontend/src/app/pages/release-manage/release-manage.component.html +++ b/src/main/frontend/src/app/pages/release-manage/release-manage.component.html @@ -80,7 +80,11 @@

General Management

Applies to all releases

- +
this.isLoading.set(false))) - .subscribe({ - next: (release) => { - this.release.set(release); - this.fetchIssues(releaseId); - this.fetchVulnerabilities(releaseId); - this.fetchBusinessValue(releaseId); - }, - error: () => this.router.navigate(['/not-found']), - }); + .getReleaseById(releaseId) + .pipe(finalize(() => this.isLoading.set(false))) + .subscribe({ + next: (release) => { + this.release.set(release); + this.fetchIssues(releaseId); + this.fetchVulnerabilities(releaseId); + this.fetchBusinessValue(releaseId); + }, + error: () => this.router.navigate(['/not-found']), + }); } private fetchIssues(releaseId: string): void { this.issueService - .getIssuesByReleaseId(releaseId) - .pipe( - catchError(() => { - this.releaseIssues.set(null); - return of(); - }), - ) - .subscribe((issues) => { - this.releaseIssues.set(issues); - }); + .getIssuesByReleaseId(releaseId) + .pipe( + catchError(() => { + this.releaseIssues.set(null); + return of(); + }), + ) + .subscribe((issues) => { + this.releaseIssues.set(issues); + }); } private fetchVulnerabilities(releaseId: string): void { this.vulnerabilityService - .getVulnerabilitiesByReleaseId(releaseId) - .pipe( - catchError(() => { - this.vulnerabilities.set(null); - return of(); - }), - ) - .subscribe((vulnerabilities) => { - this.vulnerabilities.set(vulnerabilities); - }); + .getVulnerabilitiesByReleaseId(releaseId) + .pipe( + catchError(() => { + this.vulnerabilities.set(null); + return of(); + }), + ) + .subscribe((vulnerabilities) => { + this.vulnerabilities.set(vulnerabilities); + }); } private fetchBusinessValue(releaseId: string): void { this.businessValueService - .getBusinessValuesByReleaseId(releaseId) - .pipe( - catchError(() => { - this.businessValues.set(null); - return of(); - }), - ) - .subscribe((values) => { - this.businessValues.set(values); - }); + .getBusinessValuesByReleaseId(releaseId) + .pipe( + catchError(() => { + this.businessValues.set(null); + return of(); + }), + ) + .subscribe((values) => { + this.businessValues.set(values); + }); } } From 94378a26cef840cb1477aaabba0863dd2f87c912 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Mon, 20 Jul 2026 14:48:55 +0200 Subject: [PATCH 6/8] fix: refactor tests to use Router instead of Location for navigation --- .../business-value-manage.component.spec.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts index 28cdc569..1428bfd2 100644 --- a/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts +++ b/src/main/frontend/src/app/pages/release-manage/business-value-manage/business-value-manage.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Location } from '@angular/common'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { of, throwError } from 'rxjs'; import { Component, Input, Output, EventEmitter } from '@angular/core'; import { provideHttpClient } from '@angular/common/http'; @@ -68,7 +67,7 @@ describe('BusinessValueManageComponent', () => { let mockBusinessValueService: jasmine.SpyObj; let mockIssueService: jasmine.SpyObj; let mockReleaseService: jasmine.SpyObj; - let mockLocation: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; beforeEach(async () => { mockBusinessValueService = jasmine.createSpyObj('BusinessValueService', [ @@ -79,7 +78,7 @@ describe('BusinessValueManageComponent', () => { ]); mockIssueService = jasmine.createSpyObj('IssueService', ['getIssuesByReleaseId']); mockReleaseService = jasmine.createSpyObj('ReleaseService', ['getReleaseById', 'getAllReleases']); - mockLocation = jasmine.createSpyObj('Location', ['back', 'go']); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); mockBusinessValueService.getBusinessValuesByReleaseId.and.returnValue(of(mockBusinessValues)); mockIssueService.getIssuesByReleaseId.and.returnValue(of(mockIssues)); @@ -92,7 +91,7 @@ describe('BusinessValueManageComponent', () => { { provide: BusinessValueService, useValue: mockBusinessValueService }, { provide: IssueService, useValue: mockIssueService }, { provide: ReleaseService, useValue: mockReleaseService }, - { provide: Location, useValue: mockLocation }, + { provide: Router, useValue: mockRouter }, { provide: ActivatedRoute, useValue: { @@ -304,10 +303,10 @@ describe('BusinessValueManageComponent', () => { expect(component.showCreateForm()).toBeTrue(); }); - it('should go back using location service', () => { + it('should go back to parent location with router', () => { component.goBack(); - expect(mockLocation.back).toHaveBeenCalledWith(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/release-manage', 'release-123']); }); }); }); From 21c7be1eda53bad0e79a66ba0e720764e915807b Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Tue, 21 Jul 2026 11:27:00 +0200 Subject: [PATCH 7/8] fix: refactor vulnerability selection navigation to use Router for improved routing --- .../vulnerability-impact-manage.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts index 20a7468a..6d37357c 100644 --- a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts +++ b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.ts @@ -29,8 +29,8 @@ import { ReleaseManageComponent } from '../release-manage.component'; export class VulnerabilityImpactManageComponent implements OnInit, OnDestroy { private static readonly PAGE_SIZE = 20; private static readonly SEARCH_DEBOUNCE_MS = 350; - private static readonly KEEP_LATEST_LTS_COUNT = 3; + private static readonly VULNERABILITY_MANAGE_PATH = '/vulnerabilities/manage'; public allDetails = signal([]); public selectedVulnerability = signal(null); @@ -227,10 +227,10 @@ export class VulnerabilityImpactManageComponent implements OnInit, OnDestroy { public onVulnerabilitySelected(vulnerability: Vulnerability): void { if (this.selectedVulnerability()?.cveId === vulnerability.cveId) { this.selectedVulnerability.set(null); - this.location.go('/vulnerabilities/manage'); + this.router.navigate([VulnerabilityImpactManageComponent.VULNERABILITY_MANAGE_PATH]); } else { this.selectedVulnerability.set(vulnerability); - this.location.go(`/vulnerabilities/manage/${vulnerability.cveId}`); + this.router.navigate([VulnerabilityImpactManageComponent.VULNERABILITY_MANAGE_PATH, vulnerability.cveId]); } } From d85923e21e8f7d1bfc1675037b82d7a2e9fdda70 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Tue, 21 Jul 2026 12:04:06 +0200 Subject: [PATCH 8/8] fix: refactor tests to use provideRouter and provideLocationMocks for improved routing --- .../vulnerability-impact-manage.component.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts index ae111194..c0cc1694 100644 --- a/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts +++ b/src/main/frontend/src/app/pages/release-manage/vulnerability-impact-manage/vulnerability-impact-manage.component.spec.ts @@ -2,7 +2,8 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testin import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { Location } from '@angular/common'; -import { ActivatedRoute } from '@angular/router'; +import { provideLocationMocks } from '@angular/common/testing'; +import { ActivatedRoute, provideRouter } from '@angular/router'; import { of, throwError } from 'rxjs'; import { VulnerabilityImpactManageComponent } from './vulnerability-impact-manage.component'; import { VulnerabilityService, Vulnerability, VulnerabilitySeverities, VulnerabilityPage } from '../../../services/vulnerability.service'; @@ -12,7 +13,7 @@ describe('VulnerabilityImpactManageComponent', () => { let component: VulnerabilityImpactManageComponent; let fixture: ComponentFixture; let vulnerabilityService: jasmine.SpyObj; - let location: jasmine.SpyObj; + let location: Location; let queryParameterGet: jasmine.Spy; const mockVulnerabilities: Vulnerability[] = [ @@ -60,16 +61,16 @@ describe('VulnerabilityImpactManageComponent', () => { 'deleteVulnerabilityImpact' ]); const releaseServiceSpy = jasmine.createSpyObj('ReleaseService', ['getAllReleases']); - const locationSpy = jasmine.createSpyObj('Location', ['back', 'go']); queryParameterGet = jasmine.createSpy('get').and.returnValue(null); const activatedRouteStub = { snapshot: { paramMap: { get: queryParameterGet } } }; await TestBed.configureTestingModule({ imports: [VulnerabilityImpactManageComponent], providers: [ + provideRouter([{ path: '**', component: VulnerabilityImpactManageComponent }]), + provideLocationMocks(), { provide: VulnerabilityService, useValue: vulnerabilityServiceSpy }, { provide: ReleaseService, useValue: releaseServiceSpy }, - { provide: Location, useValue: locationSpy }, { provide: ActivatedRoute, useValue: activatedRouteStub }, provideHttpClient(), provideHttpClientTesting() @@ -77,7 +78,9 @@ describe('VulnerabilityImpactManageComponent', () => { }).compileComponents(); vulnerabilityService = TestBed.inject(VulnerabilityService) as jasmine.SpyObj; - location = TestBed.inject(Location) as jasmine.SpyObj; + location = TestBed.inject(Location); + spyOn(location, 'back'); + const releaseService = TestBed.inject(ReleaseService) as jasmine.SpyObj; vulnerabilityService.getAllVulnerabilitiesPaginated.and.returnValue(of(mockPage)); @@ -246,4 +249,3 @@ describe('VulnerabilityImpactManageComponent', () => { expect(vulnerabilityService.deleteVulnerabilityImpact).not.toHaveBeenCalled(); }); }); -