From 32effee3e2e789cd5e3d7d8597addaf4730666e0 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Fri, 13 Mar 2026 12:14:20 -0400 Subject: [PATCH] fix: Fix PathwayBrowser navigation and event hierarchy tree Use route-relative navigation (navigateTo) instead of router.navigate across all PathwayBrowser components to ensure URLs stay under the /PathwayBrowser/ prefix in the umbrella app. Add missing MatTreeNodeToggle and MatTreeNodeOutlet imports to event-hierarchy component so tree expand/collapse and child rendering work correctly. Fix _ignore flag getting stuck and preserve expansion state across data source resets. Co-Authored-By: Claude Opus 4.6 --- .../tabs/result-tab/result-tab.component.ts | 2 +- .../src/app/diagram/diagram.component.ts | 4 +- .../event-hierarchy.component.ts | 55 +++++++++++++++---- .../src/app/reacfoam/reacfoam.component.ts | 2 +- .../src/app/services/species.service.ts | 2 +- .../src/app/services/url-state.service.ts | 28 ++++++++-- 6 files changed, 72 insertions(+), 21 deletions(-) diff --git a/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts b/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts index c045e9b..c1baa24 100644 --- a/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts +++ b/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts @@ -277,7 +277,7 @@ export class ResultTabComponent { visitPathway(pathway: Analysis.Pathway) { this.data.selectedPathwayStId.set(pathway.stId) console.log("Navigating to " + pathway.stId) - this.router.navigate([pathway.stId], {queryParamsHandling: 'preserve', preserveFragment: true}) + this.state.navigateTo(pathway.stId, {queryParamsHandling: 'preserve', preserveFragment: true}) } diff --git a/projects/pathway-browser/src/app/diagram/diagram.component.ts b/projects/pathway-browser/src/app/diagram/diagram.component.ts index a92f11f..6dbc6c9 100644 --- a/projects/pathway-browser/src/app/diagram/diagram.component.ts +++ b/projects/pathway-browser/src/app/diagram/diagram.component.ts @@ -397,12 +397,12 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { this.cy.on('zoom', () => this.controlZoom.set(this.zoomToControlTransform(this.cy.zoom()))); this.reactomeStyle.clearCache(); - this.cy.on('dblclick', '.SUB.Pathway', (e) => this.router.navigate([e.target.data('graph.stId')], { + this.cy.on('dblclick', '.SUB.Pathway', (e) => this.state.navigateTo(e.target.data('graph.stId'), { queryParamsHandling: "preserve", preserveFragment: true })) - this.cy.on('dblclick', '.Interacting.Pathway', (e) => this.router.navigate([e.target.data('graph.stId')], { + this.cy.on('dblclick', '.Interacting.Pathway', (e) => this.state.navigateTo(e.target.data('graph.stId'), { queryParams: {select: this.pathwayId()}, queryParamsHandling: "merge", preserveFragment: true diff --git a/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.ts b/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.ts index 53b8d1e..762ac02 100644 --- a/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.ts +++ b/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.ts @@ -3,7 +3,7 @@ import {Event} from "../model/graph/event/event.model"; import {EventService, SelectableObject} from "../services/event.service"; import {SpeciesService} from "../services/species.service"; import {combineLatest, combineLatestWith, filter, fromEvent, map, Observable, of, switchMap, take, tap} from "rxjs"; -import {MatTree, MatTreeNestedDataSource, MatTreeNodeDef} from "@angular/material/tree"; +import {MatTree, MatTreeNestedDataSource, MatTreeNodeDef, MatTreeNodeOutlet, MatTreeNodeToggle} from "@angular/material/tree"; import {UrlStateService} from "../services/url-state.service"; import {SplitComponent} from "angular-split"; import {UntilDestroy, untilDestroyed} from "@ngneat/until-destroy"; @@ -35,6 +35,8 @@ import {PassiveDirective} from "../utils/passive.directive"; MatTree, MatNestedTreeNode, MatTreeNodeDef, + MatTreeNodeToggle, + MatTreeNodeOutlet, MatButton, MatIconButton, NgClass, @@ -168,11 +170,15 @@ export class EventHierarchyComponent implements AfterViewInit, OnDestroy { }, 100); this.eventService.treeData$.pipe(untilDestroyed(this)).subscribe(events => { + // Save expanded node stIds before resetting the data source + const expandedIds = this.collectExpandedIds(this.treeDataSource.data); // @ts-ignore // Mat tree has a bug causing children to not be rendered in the UI without first setting the data to null // This is a workaround to add child data to tree and update the view. see details: https://github.com/angular/components/issues/11381 this.treeDataSource.data = []; //todo: check performance issue this.treeDataSource.data = events as Event[]; + // Restore expansion state + this.restoreExpandedIds(events as Event[], expandedIds); this.adjustWidths(); }); @@ -393,20 +399,16 @@ export class EventHierarchyComponent implements AfterViewInit, OnDestroy { const selectedEventId = isPathway(treeEvent) && treeEvent.hasDiagram ? null : treeEvent.stId; this._ignore = true; // this.speciesService.setIgnore(true); - this.router.navigate([diagramId], { + this.state.navigateTo(diagramId ?? null, { queryParamsHandling: "preserve" // Keep existing query params }).then(() => { this.state.select.set(selectedEventId); this.eventService.setCurrentTreeEvent(treeEvent); - // Listen for NavigationEnd event to reset _ignore - this.router.events.pipe( - filter(routerEvent => routerEvent instanceof NavigationEnd), - take(1) // Take the first NavigationEnd event and unsubscribe automatically - ).subscribe(() => { - this._ignore = false; - // this.speciesService.setIgnore(false); - }); - + // Reset _ignore after setting state. The .then() callback fires after + // NavigationEnd has already been emitted, so waiting for a future + // NavigationEnd would leave _ignore stuck at true if no further + // navigation occurs. + this._ignore = false; }).catch(err => { throw new Error('Navigation error:', err); }); @@ -530,4 +532,35 @@ export class EventHierarchyComponent implements AfterViewInit, OnDestroy { labelSpan.classList.remove('add-overflowX'); el.classList.remove('no-transition'); } + + private collectExpandedIds(nodes: Event[]): Set { + const expanded = new Set(); + const traverse = (items: Event[]) => { + for (const node of items) { + if (this.tree?.isExpanded(node)) { + expanded.add(node.stId); + } + if (isPathway(node) && node.events) { + traverse(node.events.map(e => e.element)); + } + } + }; + traverse(nodes); + return expanded; + } + + private restoreExpandedIds(nodes: Event[], expandedIds: Set): void { + if (expandedIds.size === 0) return; + const traverse = (items: Event[]) => { + for (const node of items) { + if (expandedIds.has(node.stId)) { + this.tree?.expand(node); + } + if (isPathway(node) && node.events) { + traverse(node.events.map(e => e.element)); + } + } + }; + traverse(nodes); + } } diff --git a/projects/pathway-browser/src/app/reacfoam/reacfoam.component.ts b/projects/pathway-browser/src/app/reacfoam/reacfoam.component.ts index af22197..77813bd 100644 --- a/projects/pathway-browser/src/app/reacfoam/reacfoam.component.ts +++ b/projects/pathway-browser/src/app/reacfoam/reacfoam.component.ts @@ -108,7 +108,7 @@ export class ReacfoamComponent implements OnDestroy { onGroupDoubleClick: (event: any) => { event.preventDefault(); - this.router.navigate([event.group.stId], {queryParamsHandling: 'preserve', preserveFragment: true}) + this.state.navigateTo(event.group.stId, {queryParamsHandling: 'preserve', preserveFragment: true}) }, onGroupClick: (event: any) => { diff --git a/projects/pathway-browser/src/app/services/species.service.ts b/projects/pathway-browser/src/app/services/species.service.ts index de142ec..440b062 100644 --- a/projects/pathway-browser/src/app/services/species.service.ts +++ b/projects/pathway-browser/src/app/services/species.service.ts @@ -121,7 +121,7 @@ export class SpeciesService { else params[key] = JSON.parse(newValue); } - this.router.navigate([pathwayId].filter(isDefined), { + this.state.navigateTo(pathwayId ?? null, { queryParams: params, preserveFragment: true }); diff --git a/projects/pathway-browser/src/app/services/url-state.service.ts b/projects/pathway-browser/src/app/services/url-state.service.ts index 16d3460..4c3f036 100644 --- a/projects/pathway-browser/src/app/services/url-state.service.ts +++ b/projects/pathway-browser/src/app/services/url-state.service.ts @@ -1,5 +1,5 @@ import {effect, inject, Injectable, signal, WritableSignal} from '@angular/core'; -import {ActivatedRoute, NavigationEnd, Params, Router} from "@angular/router"; +import {ActivatedRoute, NavigationEnd, NavigationExtras, Params, Router} from "@angular/router"; import {catchError, filter, firstValueFrom, map, of, switchMap} from "rxjs"; import {isArray, isNumber} from "lodash"; import {HttpClient} from "@angular/common/http"; @@ -112,7 +112,11 @@ export class UrlStateService implements State { constructor() { this.router.events.pipe( filter(event => event instanceof NavigationEnd), - switchMap(() => this.router.routerState.root.firstChild?.params || of()), + switchMap(() => { + let route = this.router.routerState.root; + while (route.firstChild) route = route.firstChild; + return route.params; + }), map(params => params['pathwayId']) ).subscribe((id) => { this.pathwayId.set(id) @@ -127,7 +131,7 @@ export class UrlStateService implements State { return; } - this.router.navigate(this.pathwayId() ? [this.pathwayId()] : [], { + this.navigateTo(this.pathwayId() ?? null, { queryParamsHandling: 'preserve', preserveFragment: true }); @@ -153,7 +157,7 @@ export class UrlStateService implements State { } } - this.router.navigate(id ? [id] : [], { + this.navigateTo(id ?? null, { queryParamsHandling: 'merge', fragment: fragment.replace(FRAGMENT_PATTERN, ''), preserveFragment: false, @@ -215,7 +219,21 @@ export class UrlStateService implements State { console.log('In content or search route, not navigating on state change'); return; } - this.router.navigate(this.pathwayId() ? [this.pathwayId()] : [], {queryParams, preserveFragment: true}); + this.navigateTo(this.pathwayId() ?? null, {queryParams, preserveFragment: true}); + }); + } + + /** + * Navigate to a pathway within the PathwayBrowser route context. + * Resolves the correct base path whether running standalone or inside the umbrella app. + */ + navigateTo(pathwayId: string | null, extras: NavigationExtras = {}): Promise { + let route = this.router.routerState.root; + while (route.firstChild) route = route.firstChild; + const segments = pathwayId ? [pathwayId] : []; + return this.router.navigate(segments, { + relativeTo: route.parent, + ...extras }); }