Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "1.65MB",
"maximumError": "1.8MB"
"maximumWarning": "1.4MB",
"maximumError": "1.45MB"
}
]
},
Expand All @@ -369,8 +369,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "1.65MB",
"maximumError": "1.8MB"
"maximumWarning": "1.4MB",
"maximumError": "1.45MB"
}
]
},
Expand Down
64 changes: 64 additions & 0 deletions e2e/citation-dialog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { test, expect } from './support/backend';

/**
* "Cite us" opens the citation for the pathway on screen.
*
* The dialog and its component are fetched when it is first opened rather than
* with the page (#356), so the first click has something to wait for. Nothing
* tested the dialog before; writing this found the button had no name.
*/
test('Cite us opens the citation for the pathway on screen, and closes', async ({ page }) => {
const thrown: string[] = [];
page.on('pageerror', (error) => thrown.push(String(error)));
// The request carries today's date (?dateAccessed=...), so no recording can
// match it; answered here in the service's own shape.
await page.route('**/ContentService/citation/pathway/R-HSA-69620?*', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
pathwayCitation:
"Yen, T.J. ,Khanna, K.K. ,Hoffmann, I. ,Walworth, N. & O'Donnell, M. (2005). Cell Cycle Checkpoints. Reactome, 97, https://reactome.org/content/detail/R-HSA-69620",
imageCitation:
'Image Citation for Cell Cycle Checkpoints. Reactome, 97, https://reactome.org/content/detail/R-HSA-69620',
}),
})
);
await page.goto('/PathwayBrowser/R-HSA-69620');
// By its name: it had none, only a tooltip, so a screen reader announced an
// unnamed button.
await page.getByRole('button', { name: 'Cite us' }).click({ timeout: 60_000 });
const dialog = page.locator('cr-citation');
await expect(dialog.getByRole('heading', { name: 'Cite Us!' })).toBeVisible({ timeout: 30_000 });
await expect(dialog.getByRole('heading', { name: 'Pathway' })).toBeVisible({ timeout: 30_000 });
await expect(dialog.locator('mat-dialog-content')).toContainText(
'(2005). Cell Cycle Checkpoints'
);
await dialog.locator('.close-button').click();
await expect(dialog).toHaveCount(0);
expect(thrown).toEqual([]);
});

test("the site's Cite Us button opens the citation from any page", async ({ page }) => {
const thrown: string[] = [];
page.on('pageerror', (error) => thrown.push(String(error)));
await page.goto('/');
await page.getByRole('button', { name: 'Cite Us', exact: true }).click({ timeout: 60_000 });
const dialog = page.locator('cr-citation');
await expect(dialog.getByRole('heading', { name: 'Cite Us!' })).toBeVisible({ timeout: 30_000 });
await expect(dialog.locator('mat-dialog-content')).not.toBeEmpty({ timeout: 30_000 });
expect(thrown).toEqual([]);
});

test('two quick clicks open one citation, not two', async ({ page }) => {
// The dialog's code loads on the first click, so a second click can land
// while it is still on its way; each used to open a dialog of its own.
await page.goto('/');
const button = page.getByRole('button', { name: 'Cite Us', exact: true });
await button.click({ timeout: 60_000 });
await button.click({ force: true });
const dialog = page.locator('cr-citation');
await expect(dialog.getByRole('heading', { name: 'Cite Us!' })).toBeVisible({ timeout: 30_000 });
// Long enough for a second dialog to have opened, had one been coming.
await page.waitForTimeout(2000);
await expect(dialog).toHaveCount(1);
});
1 change: 1 addition & 0 deletions e2e/har/4acadeec725de92e73791996e1f975866d71c861.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fabregat A, Sidiropoulos K, Viteri G, Marin-Garcia P, Ping P, Stein L, D'Eustachio P, Hermjakob H. Reactome diagram viewer: data structures and strategies to boost performance. Bioinformatics (Oxford, England). 2018 Apr;34(7) 1208-1214. doi: 10.1093/bioinformatics/btx752. PubMed PMID: 29186351. PubMed Central PMCID: PMC6030826.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"log":{"version":"1.2","entries":[{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/database/version"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"2"},{"name":"content-type","value":"text/plain;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"text/plain;charset=UTF-8","_file":"812ed4562d3211363a7b813aa9cd2cf042b63bb2.txt"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/species/main"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"3237"},{"name":"content-type","value":"application/json; charset=utf-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"},{"name":"etag","value":"W/\"ca5-sa7k1xX74ycTbey3jmBLCrhGTO0\""}],"content":{"mimeType":"application/json; charset=utf-8","_file":"b1aee4d715fbe327136decb78e604b0ab8464ced.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-69620?fetchIncomingRelationships=true&summariseReferenceEntity=false&includeDisease=true&includeRef=true&view=nested-aggregated"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"e71f7705e50fdd572ddc0cf27a5b6ed24c562e03.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/event/R-HSA-69620/ancestors?includeRef=true&view=nested-aggregated"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"696"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:59 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"77b1f4b5f0865f10a4a061fe36168fd1f62e604b.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-69620?fetchIncomingRelationships=true&summariseReferenceEntity=true&includeDisease=true&includeRef=true&view=nested-aggregated"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"e71f7705e50fdd572ddc0cf27a5b6ed24c562e03.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ExperimentDigester/experiments/summaries"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"5e25288a19ddb81e5e80ee5d8aa56f19cd68dd6e.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/pathways/top/9606"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"28a2743b73a578fbfc87d6ec212f2dababf479e3.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/interactors/psicquic/resources/"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"6674"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"0e84a258a3b413badb1205d66079ecb7ea99bbd0.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-69620?includeRef=true&view=nested-aggregated&summariseReferenceEntity=false&includeDisease=true"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"e71f7705e50fdd572ddc0cf27a5b6ed24c562e03.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/detail/R-HSA-69620/locationsInPWB"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"284"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"f54e01541057b3966e2f3122f73577bb12bb7d64.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/event/R-HSA-69620/ancestors"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"696"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:59 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"77b1f4b5f0865f10a4a061fe36168fd1f62e604b.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-1640170?includeRef=true&view=nested-aggregated&summariseReferenceEntity=false&includeDisease=true"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:57:01 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"a8afa20987171d392bfd782043fe5594e9842a18.json"}}}]}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"log":{"version":"1.2","entries":[{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/species/main"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"3237"},{"name":"content-type","value":"application/json; charset=utf-8"},{"name":"date","value":"Tue, 13 Oct 2026 12:08:04 GMT"},{"name":"etag","value":"W/\"ca5-sa7k1xX74ycTbey3jmBLCrhGTO0\""}],"content":{"mimeType":"application/json; charset=utf-8","_file":"b1aee4d715fbe327136decb78e604b0ab8464ced.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/database/version"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"2"},{"name":"content-type","value":"text/plain;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 12:08:04 GMT"}],"content":{"mimeType":"text/plain;charset=UTF-8","_file":"812ed4562d3211363a7b813aa9cd2cf042b63bb2.txt"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/citation/static/29186351"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"330"},{"name":"content-type","value":"text/plain;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 12:08:06 GMT"}],"content":{"mimeType":"text/plain;charset=UTF-8","_file":"4acadeec725de92e73791996e1f975866d71c861.txt"}}}]}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"log":{"version":"1.2","entries":[{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/species/main"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"3237"},{"name":"content-type","value":"application/json; charset=utf-8"},{"name":"date","value":"Tue, 13 Oct 2026 12:18:24 GMT"},{"name":"etag","value":"W/\"ca5-sa7k1xX74ycTbey3jmBLCrhGTO0\""}],"content":{"mimeType":"application/json; charset=utf-8","_file":"b1aee4d715fbe327136decb78e604b0ab8464ced.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/database/version"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"2"},{"name":"content-type","value":"text/plain;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 12:18:24 GMT"}],"content":{"mimeType":"text/plain;charset=UTF-8","_file":"812ed4562d3211363a7b813aa9cd2cf042b63bb2.txt"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/citation/static/29186351"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"330"},{"name":"content-type","value":"text/plain;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 12:18:24 GMT"}],"content":{"mimeType":"text/plain;charset=UTF-8","_file":"4acadeec725de92e73791996e1f975866d71c861.txt"}}}]}}
38 changes: 31 additions & 7 deletions projects/pathway-browser/src/app/services/citation.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { computed, inject, Injectable, Signal, signal } from '@angular/core';
import { computed, inject, Injectable, Injector, Signal, signal } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { isPathway } from './utils';
import { DataStateService } from './data-state.service';
import { AnalysisService } from './analysis.service';
import { rxResource } from '@angular/core/rxjs-interop';
import { CitationComponent } from '../citation/citation.component';
import { MatDialog } from '@angular/material/dialog';

interface PathwayCitation {
imageCitation: string;
Expand Down Expand Up @@ -38,6 +36,9 @@ export enum ExportFormat {
TXT = 'txt',
}

/** The citation dialog's id, so a second open finds the first rather than stacking. */
const CITATION_DIALOG = 'citation';

@Injectable({
providedIn: 'root',
})
Expand All @@ -46,7 +47,10 @@ export class CitationService {
private dataState = inject(DataStateService);
private analysis = inject(AnalysisService);

readonly dialog = inject(MatDialog);
// The dialog and its component are fetched when it is first opened, not with
// the page: the embeddable diagram provides this service but never shows the
// dialog, and with them it carried Material's dialog code for nothing (#356).
private readonly injector = inject(Injector);

readonly currentCitationId = signal<string | undefined>(undefined);
readonly currentCitationExportURLS = computed(() =>
Expand Down Expand Up @@ -96,9 +100,30 @@ export class CitationService {
: this.http.get<PathwayCitation>(`${pathwayUrl}`);
}

openDialog() {
async openDialog(): Promise<void> {
// One dialog at a time. The first open now waits while its code loads, and a
// reader who clicks again in that moment would otherwise get two.
if (this.opening) return;
this.opening = true;
try {
await this.showDialog();
} finally {
this.opening = false;
}
}

private opening = false;

private async showDialog(): Promise<void> {
this.currentCitationId.set(this.updatedCitationId());
const dialogRef = this.dialog.open(CitationComponent, {
const [{ MatDialog }, { CitationComponent }] = await Promise.all([
import('@angular/material/dialog'),
import('../citation/citation.component'),
]);
const dialog = this.injector.get(MatDialog);
if (dialog.getDialogById(CITATION_DIALOG)) return;
dialog.open(CitationComponent, {
id: CITATION_DIALOG,
data: {
content: this.citationData.value,
id: this.currentCitationId,
Expand All @@ -107,7 +132,6 @@ export class CitationService {
enterAnimationDuration: '450ms',
exitAnimationDuration: '450ms',
});
dialogRef.afterClosed();
}

getExportUrls(id: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,13 @@
[matTooltip]="dark.isDark() ? 'Switch to light mode' : 'Switch to dark mode'"
id="dark-toggle"
></mat-slide-toggle>
<button mat-icon-button class="quote" (click)="openCitationDialog()" matTooltip="Cite us">
<button
mat-icon-button
class="quote"
aria-label="Cite us"
(click)="openCitationDialog()"
matTooltip="Cite us"
>
<mat-icon [svgIcon]="'format-quote'"></mat-icon>
</button>
<div class="logo" matTooltip="Go to the main page">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,10 @@ export class ViewportComponent {
}

openCitationDialog() {
this.citation.openDialog();
// A failed fetch of the dialog's code would otherwise go unreported.
this.citation
.openDialog()
.catch((error) => console.error('Could not open the citation', error));
}

readonly formerPathwayBrowserURL = computed(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class CiteUsComponent {
private citation: CitationService = inject(CitationService);

openCitationDialog() {
this.citation.openDialog();
// The dialog's code is fetched on first open; a failed fetch is reported.
this.citation
.openDialog()
.catch((error) => console.error('Could not open the citation', error));
}
}
Loading
Loading