Skip to content

Commit 8297673

Browse files
authored
Merge pull request #28 from reactome/fix-pathway-tree-navigation
fix: Fix PathwayBrowser navigation and event hierarchy tree
2 parents 74b2364 + 32effee commit 8297673

6 files changed

Lines changed: 72 additions & 21 deletions

File tree

projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export class ResultTabComponent {
277277
visitPathway(pathway: Analysis.Pathway) {
278278
this.data.selectedPathwayStId.set(pathway.stId)
279279
console.log("Navigating to " + pathway.stId)
280-
this.router.navigate([pathway.stId], {queryParamsHandling: 'preserve', preserveFragment: true})
280+
this.state.navigateTo(pathway.stId, {queryParamsHandling: 'preserve', preserveFragment: true})
281281
}
282282

283283

projects/pathway-browser/src/app/diagram/diagram.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,12 +397,12 @@ export class DiagramComponent implements AfterViewInit, OnDestroy {
397397
this.cy.on('zoom', () => this.controlZoom.set(this.zoomToControlTransform(this.cy.zoom())));
398398

399399
this.reactomeStyle.clearCache();
400-
this.cy.on('dblclick', '.SUB.Pathway', (e) => this.router.navigate([e.target.data('graph.stId')], {
400+
this.cy.on('dblclick', '.SUB.Pathway', (e) => this.state.navigateTo(e.target.data('graph.stId'), {
401401
queryParamsHandling: "preserve",
402402
preserveFragment: true
403403
}))
404404

405-
this.cy.on('dblclick', '.Interacting.Pathway', (e) => this.router.navigate([e.target.data('graph.stId')], {
405+
this.cy.on('dblclick', '.Interacting.Pathway', (e) => this.state.navigateTo(e.target.data('graph.stId'), {
406406
queryParams: {select: this.pathwayId()},
407407
queryParamsHandling: "merge",
408408
preserveFragment: true

projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {Event} from "../model/graph/event/event.model";
33
import {EventService, SelectableObject} from "../services/event.service";
44
import {SpeciesService} from "../services/species.service";
55
import {combineLatest, combineLatestWith, filter, fromEvent, map, Observable, of, switchMap, take, tap} from "rxjs";
6-
import {MatTree, MatTreeNestedDataSource, MatTreeNodeDef} from "@angular/material/tree";
6+
import {MatTree, MatTreeNestedDataSource, MatTreeNodeDef, MatTreeNodeOutlet, MatTreeNodeToggle} from "@angular/material/tree";
77
import {UrlStateService} from "../services/url-state.service";
88
import {SplitComponent} from "angular-split";
99
import {UntilDestroy, untilDestroyed} from "@ngneat/until-destroy";
@@ -35,6 +35,8 @@ import {PassiveDirective} from "../utils/passive.directive";
3535
MatTree,
3636
MatNestedTreeNode,
3737
MatTreeNodeDef,
38+
MatTreeNodeToggle,
39+
MatTreeNodeOutlet,
3840
MatButton,
3941
MatIconButton,
4042
NgClass,
@@ -168,11 +170,15 @@ export class EventHierarchyComponent implements AfterViewInit, OnDestroy {
168170
}, 100);
169171

170172
this.eventService.treeData$.pipe(untilDestroyed(this)).subscribe(events => {
173+
// Save expanded node stIds before resetting the data source
174+
const expandedIds = this.collectExpandedIds(this.treeDataSource.data);
171175
// @ts-ignore
172176
// Mat tree has a bug causing children to not be rendered in the UI without first setting the data to null
173177
// This is a workaround to add child data to tree and update the view. see details: https://github.com/angular/components/issues/11381
174178
this.treeDataSource.data = []; //todo: check performance issue
175179
this.treeDataSource.data = events as Event[];
180+
// Restore expansion state
181+
this.restoreExpandedIds(events as Event[], expandedIds);
176182
this.adjustWidths();
177183
});
178184

@@ -393,20 +399,16 @@ export class EventHierarchyComponent implements AfterViewInit, OnDestroy {
393399
const selectedEventId = isPathway(treeEvent) && treeEvent.hasDiagram ? null : treeEvent.stId;
394400
this._ignore = true;
395401
// this.speciesService.setIgnore(true);
396-
this.router.navigate([diagramId], {
402+
this.state.navigateTo(diagramId ?? null, {
397403
queryParamsHandling: "preserve" // Keep existing query params
398404
}).then(() => {
399405
this.state.select.set(selectedEventId);
400406
this.eventService.setCurrentTreeEvent(treeEvent);
401-
// Listen for NavigationEnd event to reset _ignore
402-
this.router.events.pipe(
403-
filter(routerEvent => routerEvent instanceof NavigationEnd),
404-
take(1) // Take the first NavigationEnd event and unsubscribe automatically
405-
).subscribe(() => {
406-
this._ignore = false;
407-
// this.speciesService.setIgnore(false);
408-
});
409-
407+
// Reset _ignore after setting state. The .then() callback fires after
408+
// NavigationEnd has already been emitted, so waiting for a future
409+
// NavigationEnd would leave _ignore stuck at true if no further
410+
// navigation occurs.
411+
this._ignore = false;
410412
}).catch(err => {
411413
throw new Error('Navigation error:', err);
412414
});
@@ -530,4 +532,35 @@ export class EventHierarchyComponent implements AfterViewInit, OnDestroy {
530532
labelSpan.classList.remove('add-overflowX');
531533
el.classList.remove('no-transition');
532534
}
535+
536+
private collectExpandedIds(nodes: Event[]): Set<string> {
537+
const expanded = new Set<string>();
538+
const traverse = (items: Event[]) => {
539+
for (const node of items) {
540+
if (this.tree?.isExpanded(node)) {
541+
expanded.add(node.stId);
542+
}
543+
if (isPathway(node) && node.events) {
544+
traverse(node.events.map(e => e.element));
545+
}
546+
}
547+
};
548+
traverse(nodes);
549+
return expanded;
550+
}
551+
552+
private restoreExpandedIds(nodes: Event[], expandedIds: Set<string>): void {
553+
if (expandedIds.size === 0) return;
554+
const traverse = (items: Event[]) => {
555+
for (const node of items) {
556+
if (expandedIds.has(node.stId)) {
557+
this.tree?.expand(node);
558+
}
559+
if (isPathway(node) && node.events) {
560+
traverse(node.events.map(e => e.element));
561+
}
562+
}
563+
};
564+
traverse(nodes);
565+
}
533566
}

projects/pathway-browser/src/app/reacfoam/reacfoam.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class ReacfoamComponent implements OnDestroy {
108108

109109
onGroupDoubleClick: (event: any) => {
110110
event.preventDefault();
111-
this.router.navigate([event.group.stId], {queryParamsHandling: 'preserve', preserveFragment: true})
111+
this.state.navigateTo(event.group.stId, {queryParamsHandling: 'preserve', preserveFragment: true})
112112
},
113113

114114
onGroupClick: (event: any) => {

projects/pathway-browser/src/app/services/species.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class SpeciesService {
121121
else params[key] = JSON.parse(newValue);
122122
}
123123

124-
this.router.navigate([pathwayId].filter(isDefined), {
124+
this.state.navigateTo(pathwayId ?? null, {
125125
queryParams: params,
126126
preserveFragment: true
127127
});

projects/pathway-browser/src/app/services/url-state.service.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {effect, inject, Injectable, signal, WritableSignal} from '@angular/core';
2-
import {ActivatedRoute, NavigationEnd, Params, Router} from "@angular/router";
2+
import {ActivatedRoute, NavigationEnd, NavigationExtras, Params, Router} from "@angular/router";
33
import {catchError, filter, firstValueFrom, map, of, switchMap} from "rxjs";
44
import {isArray, isNumber} from "lodash";
55
import {HttpClient} from "@angular/common/http";
@@ -112,7 +112,11 @@ export class UrlStateService implements State {
112112
constructor() {
113113
this.router.events.pipe(
114114
filter(event => event instanceof NavigationEnd),
115-
switchMap(() => this.router.routerState.root.firstChild?.params || of()),
115+
switchMap(() => {
116+
let route = this.router.routerState.root;
117+
while (route.firstChild) route = route.firstChild;
118+
return route.params;
119+
}),
116120
map(params => params['pathwayId'])
117121
).subscribe((id) => {
118122
this.pathwayId.set(id)
@@ -127,7 +131,7 @@ export class UrlStateService implements State {
127131
return;
128132
}
129133

130-
this.router.navigate(this.pathwayId() ? [this.pathwayId()] : [], {
134+
this.navigateTo(this.pathwayId() ?? null, {
131135
queryParamsHandling: 'preserve',
132136
preserveFragment: true
133137
});
@@ -153,7 +157,7 @@ export class UrlStateService implements State {
153157
}
154158
}
155159

156-
this.router.navigate(id ? [id] : [], {
160+
this.navigateTo(id ?? null, {
157161
queryParamsHandling: 'merge',
158162
fragment: fragment.replace(FRAGMENT_PATTERN, ''),
159163
preserveFragment: false,
@@ -215,7 +219,21 @@ export class UrlStateService implements State {
215219
console.log('In content or search route, not navigating on state change');
216220
return;
217221
}
218-
this.router.navigate(this.pathwayId() ? [this.pathwayId()] : [], {queryParams, preserveFragment: true});
222+
this.navigateTo(this.pathwayId() ?? null, {queryParams, preserveFragment: true});
223+
});
224+
}
225+
226+
/**
227+
* Navigate to a pathway within the PathwayBrowser route context.
228+
* Resolves the correct base path whether running standalone or inside the umbrella app.
229+
*/
230+
navigateTo(pathwayId: string | null, extras: NavigationExtras = {}): Promise<boolean> {
231+
let route = this.router.routerState.root;
232+
while (route.firstChild) route = route.firstChild;
233+
const segments = pathwayId ? [pathwayId] : [];
234+
return this.router.navigate(segments, {
235+
relativeTo: route.parent,
236+
...extras
219237
});
220238
}
221239

0 commit comments

Comments
 (0)