Skip to content
Open
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
Binary file modified docs/explore/images/duplicate-badges.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/explore/images/structure-viewer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/explore/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The legend shows the current coloring scheme:

## Structure Viewer

When you select a protein, the 3D viewer appears and fetches its structure from AlphaFold (if available). The viewer includes direct links to the [AlphaFold Database](https://alphafold.ebi.ac.uk/), [UniProt](https://www.uniprot.org/), and [InterPro](https://www.interpro.org/) entries for the selected protein.
When you select a protein, the 3D viewer appears and fetches its structure from AlphaFold (if available). The viewer includes direct links to the [AlphaFold Database](https://alphafold.ebi.ac.uk/), [UniProt](https://www.uniprot.org/), [InterPro](https://www.interpro.org/), and [TED](https://ted.cathdb.info/) entries for the selected protein.

<img src="./images/structure-viewer.png" alt="Structure Viewer - showing 3D protein structure" style="max-width: 50%; display: block; margin: 1em 0;" />

Expand Down
2 changes: 1 addition & 1 deletion docs/explore/structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ProtSpace integrates with AlphaFold to display 3D protein structures alongside y
When you select a protein with a UniProt accession:

1. The structure viewer appears in the sidebar below the legend
2. Links to [AlphaFold Database](https://alphafold.ebi.ac.uk/), [UniProt](https://www.uniprot.org/), and [InterPro](https://www.interpro.org/) appear at the top - click them anytime
2. Links to [AlphaFold Database](https://alphafold.ebi.ac.uk/), [UniProt](https://www.uniprot.org/), [InterPro](https://www.interpro.org/), and [TED](https://ted.cathdb.info/) appear at the top - click them anytime
3. The AlphaFold structure file is fetched directly from the [AlphaFold Database API](https://alphafold.ebi.ac.uk/api-docs); the [3D-Beacons API](https://www.ebi.ac.uk/pdbe/pdbe-kb/3dbeacons/) is used only to look up the model page link

::: tip Supported Structures
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-ted-link/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-01
3 changes: 3 additions & 0 deletions openspec/changes/add-ted-link/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# add-ted-link

Add a TED external-resource link to the protein structure viewer for issue #344.
46 changes: 46 additions & 0 deletions openspec/changes/add-ted-link/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## Context

The structure viewer header currently renders AlphaFold, UniProt, and InterPro destinations. Each destination URL is assembled in `header-links.ts`; UniProt and InterPro share `getBaseAccession()` so versioned protein IDs target the canonical accession. Issue #344 requests a TED link using `https://ted.cathdb.info/uniprot/<UniProtID>`.

## Goals / Non-Goals

**Goals:**

- Render TED beside the existing UniProt and InterPro links for every selected protein.
- Reuse the existing base-accession normalization and URL encoding contract.
- Preserve the existing safe new-tab attributes and visual treatment.
- Protect the URL builder and rendered behavior with focused tests.

**Non-Goals:**

- Redesign the structure viewer header or link styles.
- Add TED annotations, data fetching, availability checks, or navigation tracking.
- Refactor all resource links into a new abstraction.

## Decisions

### Extend the existing helper-and-template pattern

Add a pure `buildTedUrl()` helper beside the existing destination builders, then render TED as a sibling header link separated by the existing middle-dot element. This keeps normalization in one tested boundary and follows the current component structure.

Alternatives considered:

- **Inline the TED URL in the Lit template.** Smaller in line count, but it duplicates normalization/encoding behavior and makes the URL contract harder to test independently.
- **Replace all links with a resource-descriptor array.** This could reduce repeated markup, but it expands issue scope and refactors working links for no user benefit.

### Test the observable link and the URL boundary

Add a jsdom component regression that renders a versioned protein ID and asserts that the real TED anchor has the expected href, new-tab target, and rel attributes. Add focused pure-helper cases for the exact TED pattern, version stripping, and encoding. The component test prevents an unused builder from appearing to fix the issue.

## Risks / Trade-offs

- **TED may not have a page for every UniProt accession** → Match the existing UniProt/InterPro behavior: expose the deterministic destination and let the external service report availability.
- **The extra label could tighten header space** → Reuse the existing wrapping flex container and compact link styles; verify the rendered desktop flow without introducing new layout rules.

## Migration Plan

No migration is required. The change is additive and can be rolled back by reverting the helper, anchor, tests, and spec artifacts.

## Open Questions

None. The issue supplies the canonical URL format and the existing header establishes placement and interaction behavior.
29 changes: 29 additions & 0 deletions openspec/changes/add-ted-link/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Why

The protein structure viewer links selected proteins to UniProt and InterPro, but it omits TED even though TED provides a directly addressable UniProt-based protein page. Adding the missing link lets users move from a selected ProtSpace protein to its TED domain predictions without manually reconstructing the URL.

## What Changes

- Add TED as an external resource in the structure viewer header beside UniProt and InterPro.
- Build TED URLs from the normalized base UniProt accession used by the existing resource links.
- Add regression coverage for the URL contract and rendered header link.
- Update the Explore documentation and generated structure-viewer screenshot to show TED,
including stale image-pipeline readiness checks that blocked regeneration.

## Capabilities

### New Capabilities

- `protein-resource-links`: External protein-resource links exposed by the structure viewer, including accession normalization and safe new-tab behavior.

### Modified Capabilities

None.

## Impact

- Affects the structure viewer header and its pure URL-building helpers in `packages/core`.
- Adds focused Vitest coverage in the same package.
- Updates the Explore resource-link descriptions, their shared generated screenshot, and the
image-pipeline readiness checks.
- Adds no dependencies, API changes, data migrations, or styling changes.
30 changes: 30 additions & 0 deletions openspec/changes/add-ted-link/specs/protein-resource-links/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## ADDED Requirements

### Requirement: Structure viewer exposes protein resource links

When a protein is selected and the structure viewer header is shown, the system SHALL expose UniProt, InterPro, and TED as external resource links for that protein.

#### Scenario: TED link is shown with existing protein resources

- **WHEN** the structure viewer renders a selected protein
- **THEN** its header shows a link named `TED` beside the UniProt and InterPro links
- **AND** the TED link opens in a new tab without granting the destination access to the opener

### Requirement: TED link targets the canonical UniProt accession

The system SHALL build the TED destination as `https://ted.cathdb.info/uniprot/<accession>`, where `<accession>` is the URL-encoded base accession before any version suffix.

#### Scenario: Unversioned accession targets TED

- **WHEN** the selected protein ID is `W6JQJ9`
- **THEN** the TED link target is `https://ted.cathdb.info/uniprot/W6JQJ9`

#### Scenario: Versioned accession targets its base entry

- **WHEN** the selected protein ID is `W6JQJ9.2`
- **THEN** the TED link target is `https://ted.cathdb.info/uniprot/W6JQJ9`

#### Scenario: Accession is safely encoded

- **WHEN** a protein ID contains characters that are not safe in a URL path segment
- **THEN** the base accession is URL-encoded in the TED link target
17 changes: 17 additions & 0 deletions openspec/changes/add-ted-link/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## 1. Regression Coverage

- [x] 1.1 Add focused TED URL-builder and rendered-header regression tests.
- [x] 1.2 Run the focused tests against the current implementation and record the expected RED failure.

## 2. Minimal Implementation

- [x] 2.1 Add the TED URL builder using the existing base-accession normalization.
- [x] 2.2 Render the TED anchor beside UniProt and InterPro with matching safe new-tab behavior.
- [x] 2.3 Run the focused tests and record GREEN.

## 3. Verification

- [x] 3.1 Repeat the original browser reproduction and verify the TED label and exact href.
- [x] 3.2 Run the affected package checks and the repository-mandated `pnpm precommit` gate.
- [x] 3.3 Repair the stale image-pipeline readiness checks, update the Explore descriptions, and
regenerate their shared structure-viewer screenshot.
11 changes: 11 additions & 0 deletions packages/core/src/components/structure-viewer/header-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
buildAlphaFoldUrl,
buildUniProtUrl,
buildInterProUrl,
buildTedUrl,
} from './header-links';

describe('header-links', () => {
Expand Down Expand Up @@ -76,4 +77,14 @@ describe('header-links', () => {
);
});
});

describe('buildTedUrl', () => {
it('builds a TED URL from the base accession', () => {
expect(buildTedUrl('W6JQJ9.2')).toBe('https://ted.cathdb.info/uniprot/W6JQJ9');
});

it('encodes special characters in the accession', () => {
expect(buildTedUrl('A B')).toBe('https://ted.cathdb.info/uniprot/A%20B');
});
});
});
7 changes: 7 additions & 0 deletions packages/core/src/components/structure-viewer/header-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ export function buildUniProtUrl(proteinId: string): string {
export function buildInterProUrl(proteinId: string): string {
return `https://www.ebi.ac.uk/interpro/protein/UniProt/${encodeURIComponent(getBaseAccession(proteinId))}/`;
}

/**
* Build the TED protein page URL for a protein.
*/
export function buildTedUrl(proteinId: string): string {
return `https://ted.cathdb.info/uniprot/${encodeURIComponent(getBaseAccession(proteinId))}`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import './structure-viewer';

type StructureViewerElement = HTMLElement & {
autoSync: boolean;
proteinId: string | null;
updateComplete: Promise<unknown>;
};

describe('protspace-structure-viewer resource links', () => {
beforeEach(() => {
document.body.innerHTML = '';
vi.stubGlobal(
'requestAnimationFrame',
vi.fn(() => 1),
);
});

afterEach(() => {
document.body.innerHTML = '';
vi.unstubAllGlobals();
});

it('renders TED beside the existing protein resources', async () => {
const viewer = document.createElement('protspace-structure-viewer') as StructureViewerElement;
viewer.autoSync = false;
viewer.proteinId = 'W6JQJ9.2';
document.body.appendChild(viewer);
await viewer.updateComplete;

const tedLink = Array.from(
viewer.shadowRoot!.querySelectorAll<HTMLAnchorElement>('.header-link'),
).find((link) => link.textContent?.trim() === 'TED');

expect({
href: tedLink?.getAttribute('href'),
rel: tedLink?.getAttribute('rel'),
target: tedLink?.getAttribute('target'),
}).toEqual({
href: 'https://ted.cathdb.info/uniprot/W6JQJ9',
rel: 'noopener noreferrer',
target: '_blank',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { StructureService } from '@protspace/utils';
import type { StructureData } from '@protspace/utils';
import { structureViewerStyles } from './structure-viewer.styles';
import { createMolstarViewer, type MolstarViewer } from './molstar-loader';
import { buildAlphaFoldUrl, buildUniProtUrl, buildInterProUrl } from './header-links';
import { buildAlphaFoldUrl, buildInterProUrl, buildTedUrl, buildUniProtUrl } from './header-links';
import {
createStructureErrorEventDetail,
createStructureLoadDetail,
Expand Down Expand Up @@ -340,6 +340,16 @@ export class ProtspaceStructureViewer extends LitElement {
>
InterPro
</a>
<span class="header-link-separator">&middot;</span>
<a
class="header-link"
href=${buildTedUrl(this.proteinId)}
target="_blank"
rel="noopener noreferrer"
title="Open in TED"
>
TED
</a>
</span>
</div>
<div class="header-actions">
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default defineConfig({
// For better WebGL on CI, consider using xvfb with headless: false
headless: !!process.env.CI,
},
testMatch: /capture-static\.spec\.ts/,
testMatch: /capture-(?:helpers|static)\.spec\.ts/,
},
{
name: 'animations',
Expand Down
Loading
Loading