Skip to content

Commit 23bf408

Browse files
feat: Add detail pages with Locations in the PathwayBrowser tree
Add /content/detail/:id route with a full detail page that includes: - Description tab (reusing PathwayBrowser's DescriptionTabComponent) - Locations in the PathwayBrowser section showing hierarchical tree of where an entity/event exists in Reactome's pathway hierarchy - Provider stubs for services required by the description tab - Animations support and PDBe Molstar for structure visualization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9608181 commit 23bf408

17 files changed

Lines changed: 810 additions & 0 deletions

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ import {SpeciesService} from "../../../services/species.service";
5050
import {Summation} from "../../../model/graph/summation.model";
5151
import {FigureService} from "./figure/figure.service";
5252
import HasModifiedResidue = Relationship.HasModifiedResidue;
53+
import {KeyValuePipe, NgClass, NgTemplateOutlet} from "@angular/common";
54+
import {RouterLink} from "@angular/router";
55+
import {SortByTextPipe} from "../../../pipes/sort-by-text.pipe";
56+
import {SortByDatePipe} from "../../../pipes/sort-by-date.pipe";
57+
import {IncludeRefPipe} from "../../../pipes/include-ref.pipe";
58+
import {AuthorshipDateFormatPipe} from "../../../pipes/authorship-date-format.pipe";
5359
import {MatDivider} from "@angular/material/divider";
5460
import {MatIcon} from "@angular/material/icon";
5561
import {MatTooltip} from "@angular/material/tooltip";
@@ -76,6 +82,14 @@ import {InteractorsTableComponent} from "../../common/interactors-table/interact
7682
styleUrl: './description-tab.component.scss',
7783
standalone: true,
7884
imports: [
85+
NgTemplateOutlet,
86+
NgClass,
87+
KeyValuePipe,
88+
RouterLink,
89+
SortByTextPipe,
90+
SortByDatePipe,
91+
IncludeRefPipe,
92+
AuthorshipDateFormatPipe,
7993
MatDivider,
8094
MatIcon,
8195
MatTooltip,

projects/website-angular/src/app/app.routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const routes: Routes = [
3636
{ path: 'content/schema', loadComponent: () => import('./content/schema/schema.component').then(m => m.SchemaComponent), pathMatch: 'full' },
3737
{ path: 'content/schema/:className', loadComponent: () => import('./content/schema/schema.component').then(m => m.SchemaComponent), pathMatch: 'full' },
3838

39+
//Detail Pages
40+
{ path: 'content/detail/:id', loadComponent: () => import('./content/detail/detail.component').then(m => m.DetailComponent) },
41+
3942
//Search Pages
4043
{ path: 'content/query', loadComponent: () => import('./search/search.component').then(m => m.SearchComponent) },
4144
{ path: 'tools/site-search', loadComponent: () => import('./site-search/site-search.component').then(m => m.SiteSearchComponent) },
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<app-page-layout [showSidebar]="false" [showBreadcrumb]="false">
2+
@if (loading()) {
3+
<div class="loading-container">
4+
<mat-spinner diameter="48"></mat-spinner>
5+
</div>
6+
} @else if (error()) {
7+
<div class="error-container">
8+
<h2>Entity not found</h2>
9+
<p>The requested entity could not be found.</p>
10+
</div>
11+
} @else if (obj()) {
12+
<div class="detail-container">
13+
@if (entityId()) {
14+
<app-locations-tree [id]="entityId()!" />
15+
}
16+
<cr-description-tab [obj]="obj()!" />
17+
</div>
18+
}
19+
</app-page-layout>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
:host {
2+
display: block;
3+
}
4+
5+
.loading-container {
6+
display: flex;
7+
justify-content: center;
8+
align-items: center;
9+
min-height: 300px;
10+
}
11+
12+
.error-container {
13+
display: flex;
14+
flex-direction: column;
15+
align-items: center;
16+
justify-content: center;
17+
min-height: 300px;
18+
color: var(--on-surface);
19+
20+
h2 {
21+
margin-bottom: 8px;
22+
}
23+
24+
p {
25+
color: var(--outline);
26+
}
27+
}
28+
29+
.detail-container {
30+
container: bottom-panel / inline-size;
31+
width: 100%;
32+
}
33+
34+
::ng-deep cr-description-tab {
35+
// Double the ID selector to beat Angular's encapsulated
36+
// #details[_ngcontent-xxx] specificity (0,2,0) with (0,2,1)
37+
#details#details {
38+
height: auto;
39+
overflow-y: visible;
40+
overflow-x: visible;
41+
padding: 0;
42+
}
43+
44+
#details {
45+
.content {
46+
min-width: 0;
47+
overflow-x: auto;
48+
}
49+
50+
.details-button {
51+
display: none;
52+
}
53+
54+
.toc {
55+
height: auto;
56+
max-height: calc(100vh - 100px);
57+
overflow-y: auto;
58+
position: sticky;
59+
top: 80px;
60+
}
61+
}
62+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {Component, inject, OnInit, signal} from '@angular/core';
2+
import {ActivatedRoute} from '@angular/router';
3+
import {DomSanitizer} from '@angular/platform-browser';
4+
import {MatIconRegistry} from '@angular/material/icon';
5+
import {MatProgressSpinner} from '@angular/material/progress-spinner';
6+
import {of} from 'rxjs';
7+
8+
import {PageLayoutComponent} from '../../page-layout/page-layout.component';
9+
import {
10+
DescriptionTabComponent
11+
} from '../../../../../pathway-browser/src/app/details/tabs/description-tab/description-tab.component';
12+
import {SelectableObject} from '../../../../../pathway-browser/src/app/services/event.service';
13+
import {UrlStateService} from '../../../../../pathway-browser/src/app/services/url-state.service';
14+
import {DataStateService} from '../../../../../pathway-browser/src/app/services/data-state.service';
15+
import {EntityService} from '../../../../../pathway-browser/src/app/services/entity.service';
16+
import {InteractorService} from '../../../../../pathway-browser/src/app/interactors/services/interactor.service';
17+
import {FigureService} from '../../../../../pathway-browser/src/app/details/tabs/description-tab/figure/figure.service';
18+
import {SpeciesService} from '../../../../../pathway-browser/src/app/services/species.service';
19+
import {DiagramService} from '../../../../../pathway-browser/src/app/services/diagram.service';
20+
import {ParticipantService} from '../../../../../pathway-browser/src/app/services/participant.service';
21+
import {IconService} from '../../../../../pathway-browser/src/app/services/icon.service';
22+
23+
import {LocationsTreeComponent} from './locations-tree/locations-tree.component';
24+
import {DetailDataService} from '../../../services/detail-data.service';
25+
import {DetailUrlState} from './providers/detail-url-state.provider';
26+
import {DetailDataState} from './providers/detail-data-state.provider';
27+
import {DetailEntityService} from './providers/detail-entity.provider';
28+
import {DetailInteractorService} from './providers/detail-interactor.provider';
29+
import {DetailFigureService} from './providers/detail-figure.provider';
30+
import {DetailSpeciesService} from './providers/detail-species.provider';
31+
32+
@Component({
33+
selector: 'app-detail',
34+
standalone: true,
35+
imports: [PageLayoutComponent, DescriptionTabComponent, LocationsTreeComponent, MatProgressSpinner],
36+
providers: [
37+
{provide: UrlStateService, useClass: DetailUrlState},
38+
{provide: DataStateService, useClass: DetailDataState},
39+
{provide: EntityService, useClass: DetailEntityService},
40+
{provide: InteractorService, useClass: DetailInteractorService},
41+
{provide: FigureService, useClass: DetailFigureService},
42+
{provide: SpeciesService, useClass: DetailSpeciesService},
43+
{provide: DiagramService, useValue: {}},
44+
{provide: ParticipantService, useValue: {getReferenceEntities: () => of([])}},
45+
],
46+
templateUrl: './detail.component.html',
47+
styleUrl: './detail.component.scss',
48+
})
49+
export class DetailComponent implements OnInit {
50+
private route = inject(ActivatedRoute);
51+
private detailDataService = inject(DetailDataService);
52+
private dataState = inject(DataStateService) as unknown as DetailDataState;
53+
private matIconRegistry = inject(MatIconRegistry);
54+
private domSanitizer = inject(DomSanitizer);
55+
private iconService = inject(IconService);
56+
57+
entityId = signal<string | null>(null);
58+
obj = signal<SelectableObject | undefined>(undefined);
59+
loading = signal(true);
60+
error = signal(false);
61+
62+
constructor() {
63+
this.registerIcons();
64+
}
65+
66+
private registerIcons() {
67+
const speciesIcons = this.iconService.getSpeciesIcons();
68+
const generalIcons = this.iconService.getGeneralIcons();
69+
const reactomeSubjectIcons = this.iconService.getReactomeSubjectIcons();
70+
71+
this.matIconRegistry.registerFontClassAlias('symbols', 'material-symbols-rounded');
72+
73+
speciesIcons.forEach(icon => {
74+
this.matIconRegistry.addSvgIcon(icon.name, this.domSanitizer.bypassSecurityTrustResourceUrl(`assets/icons/species/${icon.route}.svg`));
75+
});
76+
77+
generalIcons.forEach(icon => {
78+
this.matIconRegistry.addSvgIcon(icon.name, this.domSanitizer.bypassSecurityTrustResourceUrl(`assets/icons/general/${icon.route}.svg`));
79+
});
80+
81+
Object.values(reactomeSubjectIcons).forEach((icon) => {
82+
this.matIconRegistry.addSvgIcon(icon.name, this.domSanitizer.bypassSecurityTrustResourceUrl(`assets/icons/reactome-subject/${icon.route}.svg`));
83+
});
84+
}
85+
86+
ngOnInit() {
87+
const id = this.route.snapshot.paramMap.get('id');
88+
this.entityId.set(id);
89+
if (!id) {
90+
this.loading.set(false);
91+
this.error.set(true);
92+
return;
93+
}
94+
95+
this.detailDataService.fetchEnhancedData<SelectableObject>(id).subscribe({
96+
next: (data) => {
97+
if (data) {
98+
this.obj.set(data);
99+
this.dataState.selectedElement.set(data);
100+
} else {
101+
this.error.set(true);
102+
}
103+
this.loading.set(false);
104+
},
105+
error: () => {
106+
this.error.set(true);
107+
this.loading.set(false);
108+
}
109+
});
110+
}
111+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@if (trees().length) {
2+
<div class="locations-section">
3+
<div class="header">
4+
<div class="label">
5+
<span class="name">Locations in the PathwayBrowser</span>
6+
</div>
7+
<button class="expand-all-btn" (click)="toggleAll()">
8+
{{ allExpanded() ? 'Collapse all' : 'Expand all' }}
9+
</button>
10+
</div>
11+
12+
<div class="tree-list">
13+
@for (root of trees(); track nodeKey(root)) {
14+
<ng-container
15+
*ngTemplateOutlet="treeNode; context: { $implicit: root, level: 0 }"
16+
/>
17+
}
18+
</div>
19+
</div>
20+
}
21+
22+
<ng-template #treeNode let-node let-level="level">
23+
<div class="tree-node" [style.padding-left.px]="level * 20">
24+
<div class="node-row">
25+
@if (hasChildren(node)) {
26+
<button class="toggle-btn" (click)="toggleNode(nodeKey(node))">
27+
<mat-icon class="expand-icon" fontSet="symbols">
28+
{{ isExpanded(nodeKey(node)) ? 'expand_more' : 'chevron_right' }}
29+
</mat-icon>
30+
</button>
31+
} @else {
32+
<span class="toggle-placeholder"></span>
33+
}
34+
<mat-icon class="type-icon" [svgIcon]="getIconName(node.type)"></mat-icon>
35+
@if (hasChildren(node)) {
36+
<span class="node-name">{{ node.name }}</span>
37+
} @else {
38+
<a class="node-name leaf-link" [href]="node.url">{{ node.name }}</a>
39+
}
40+
@if (node.species) {
41+
<span class="node-species">({{ node.species }})</span>
42+
}
43+
</div>
44+
@if (hasChildren(node) && isExpanded(nodeKey(node))) {
45+
@for (child of node.children; track nodeKey(child)) {
46+
<ng-container
47+
*ngTemplateOutlet="treeNode; context: { $implicit: child, level: level + 1 }"
48+
/>
49+
}
50+
}
51+
</div>
52+
</ng-template>

0 commit comments

Comments
 (0)