jbrowse-plugin-protein3d 0.16.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/AddHighlightModel/Highlight.js +2 -2
  2. package/dist/AddHighlightModel/index.js +2 -11
  3. package/dist/AddHighlightModel/util.js +0 -20
  4. package/dist/LaunchProteinView/codingFeature.js +1 -15
  5. package/dist/LaunchProteinView/components/PdbSearch.js +18 -4
  6. package/dist/LaunchProteinView/components/proteinTrackSetup.js +17 -37
  7. package/dist/LaunchProteinView/components/wiggleBandColors.js +3 -17
  8. package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +5 -2
  9. package/dist/LaunchProteinView/hooks/useUniProtIdLookup.js +11 -15
  10. package/dist/LaunchProteinView/index.js +27 -55
  11. package/dist/LaunchProteinView/services/foldseekApi.js +33 -8
  12. package/dist/LaunchProteinView/utils/launchHelpers.js +3 -3
  13. package/dist/LaunchProteinView/utils/launchViewUtils.js +5 -3
  14. package/dist/LaunchProteinView/utils/sideBySide.js +6 -29
  15. package/dist/LaunchProteinView/utils/translateTranscripts.js +3 -12
  16. package/dist/LaunchProteinView/utils/util.js +2 -1
  17. package/dist/LaunchProteinViewExtensionPoint/index.js +4 -7
  18. package/dist/LaunchProteinViewExtensionPoint/resolveShortLaunch.js +3 -23
  19. package/dist/Protein1DLinkage/index.js +1 -2
  20. package/dist/ProteinView/alignOffThread.js +1 -5
  21. package/dist/ProteinView/components/ChainSelect.js +6 -3
  22. package/dist/ProteinView/components/FeatureBar.js +1 -1
  23. package/dist/ProteinView/components/HeaderStructureRow.js +13 -4
  24. package/dist/ProteinView/components/ManualAlignmentDialog.js +14 -18
  25. package/dist/ProteinView/components/ProteinViewHeader.js +2 -2
  26. package/dist/ProteinView/entityAlignedTo.js +1 -1
  27. package/dist/ProteinView/frameSelection.js +10 -4
  28. package/dist/ProteinView/model.js +14 -7
  29. package/dist/ProteinView/molstarExports.js +1 -0
  30. package/dist/ProteinView/storedSettings.js +17 -1
  31. package/dist/ProteinView/structureLoader.js +12 -2
  32. package/dist/ProteinView/structureModel.js +93 -56
  33. package/dist/ProteinView/structureSuperposer.js +23 -14
  34. package/dist/ProteinView/structureVisibility.js +17 -0
  35. package/dist/chunks/{HelpDialog-KAG73BNB.js → HelpDialog-I4UK7TQB.js} +2 -2
  36. package/dist/chunks/{ProteinAlignmentHelpDialog-L2GH5YTM.js → ProteinAlignmentHelpDialog-6VFZFN6X.js} +2 -2
  37. package/dist/chunks/ProteinView-CFQG6IHO.js +9 -0
  38. package/dist/chunks/ProteinView-CFQG6IHO.js.map +7 -0
  39. package/dist/chunks/chunk-V2WE2V7Z.js +16 -0
  40. package/dist/chunks/chunk-V2WE2V7Z.js.map +7 -0
  41. package/dist/chunks/{molstarExports-HAGQ7LSM.js → molstarExports-YN6TKVEW.js} +36 -36
  42. package/dist/jbrowse-plugin-protein3d.esm.js +7 -7
  43. package/dist/jbrowse-plugin-protein3d.esm.js.map +4 -4
  44. package/dist/version.js +1 -1
  45. package/package.json +2 -15
  46. package/src/AddHighlightModel/Highlight.tsx +2 -2
  47. package/src/AddHighlightModel/index.test.ts +7 -9
  48. package/src/AddHighlightModel/index.tsx +4 -17
  49. package/src/AddHighlightModel/proteinViewLookup.test.ts +3 -1
  50. package/src/AddHighlightModel/util.ts +0 -30
  51. package/src/LaunchProteinView/codingFeature.test.ts +1 -44
  52. package/src/LaunchProteinView/codingFeature.ts +2 -22
  53. package/src/LaunchProteinView/components/PdbSearch.tsx +21 -5
  54. package/src/LaunchProteinView/components/launchProteinAnnotationView.ts +2 -3
  55. package/src/LaunchProteinView/components/proteinAssemblySetup.ts +2 -2
  56. package/src/LaunchProteinView/components/proteinTrackSetup.ts +24 -47
  57. package/src/LaunchProteinView/components/wiggleBandColors.test.ts +3 -30
  58. package/src/LaunchProteinView/components/wiggleBandColors.ts +3 -22
  59. package/src/LaunchProteinView/hooks/useAlphaFoldData.test.ts +44 -0
  60. package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +6 -3
  61. package/src/LaunchProteinView/hooks/useUniProtIdLookup.ts +14 -18
  62. package/src/LaunchProteinView/index.ts +39 -74
  63. package/src/LaunchProteinView/menuTarget.test.ts +7 -14
  64. package/src/LaunchProteinView/services/foldseekApi.test.ts +43 -0
  65. package/src/LaunchProteinView/services/foldseekApi.ts +40 -12
  66. package/src/LaunchProteinView/utils/launchHelpers.ts +3 -3
  67. package/src/LaunchProteinView/utils/launchViewUtils.ts +12 -6
  68. package/src/LaunchProteinView/utils/sideBySide.test.ts +0 -6
  69. package/src/LaunchProteinView/utils/sideBySide.ts +6 -29
  70. package/src/LaunchProteinView/utils/translateTranscripts.test.ts +9 -1
  71. package/src/LaunchProteinView/utils/translateTranscripts.ts +3 -15
  72. package/src/LaunchProteinView/utils/util.ts +2 -1
  73. package/src/LaunchProteinViewExtensionPoint/index.ts +4 -13
  74. package/src/LaunchProteinViewExtensionPoint/resolveShortLaunch.ts +6 -33
  75. package/src/Protein1DLinkage/index.ts +1 -2
  76. package/src/ProteinView/alignOffThread.ts +1 -5
  77. package/src/ProteinView/components/ChainSelect.tsx +8 -3
  78. package/src/ProteinView/components/FeatureBar.tsx +1 -1
  79. package/src/ProteinView/components/HeaderStructureRow.tsx +30 -3
  80. package/src/ProteinView/components/ManualAlignmentDialog.tsx +19 -25
  81. package/src/ProteinView/components/ProteinViewHeader.tsx +0 -2
  82. package/src/ProteinView/connectedHover.test.ts +3 -1
  83. package/src/ProteinView/entityAlignedTo.ts +3 -1
  84. package/src/ProteinView/frameSelection.test.ts +46 -1
  85. package/src/ProteinView/frameSelection.ts +16 -5
  86. package/src/ProteinView/geneExplorerLinkage.test.ts +3 -2
  87. package/src/ProteinView/kyteDoolittleColorTheme.test.ts +4 -1
  88. package/src/ProteinView/mappedChainColorTheme.test.ts +4 -1
  89. package/src/ProteinView/model.test.ts +13 -6
  90. package/src/ProteinView/model.ts +20 -10
  91. package/src/ProteinView/molstarExports.ts +1 -0
  92. package/src/ProteinView/proteinViewSpec.ts +2 -0
  93. package/src/ProteinView/storedSettings.ts +18 -2
  94. package/src/ProteinView/structureLoader.test.ts +34 -1
  95. package/src/ProteinView/structureLoader.ts +14 -2
  96. package/src/ProteinView/structureModel.test.ts +112 -3
  97. package/src/ProteinView/structureModel.ts +106 -67
  98. package/src/ProteinView/structureSuperposer.test.ts +46 -0
  99. package/src/ProteinView/structureSuperposer.ts +26 -19
  100. package/src/ProteinView/structureVisibility.ts +33 -0
  101. package/src/ProteinView/viewInteractions.ts +0 -1
  102. package/src/ProteinView/withStoredSettings.test.ts +30 -5
  103. package/src/UniProtVariationAdapter/parseUniProtVariants.test.ts +4 -3
  104. package/src/version.ts +1 -1
  105. package/dist/LaunchProteinView/utils/sessionWithAddTracks.js +0 -6
  106. package/dist/ProteinView/proteinAbbreviationMapping.js +0 -22
  107. package/dist/chunks/ProteinView-7GUUQZL4.js +0 -9
  108. package/dist/chunks/ProteinView-7GUUQZL4.js.map +0 -7
  109. package/dist/chunks/chunk-3IKI3UVA.js +0 -16
  110. package/dist/chunks/chunk-3IKI3UVA.js.map +0 -7
  111. package/dist/extendStateModel.js +0 -9
  112. package/src/LaunchProteinView/utils/sessionWithAddTracks.ts +0 -15
  113. package/src/ProteinView/proteinAbbreviationMapping.ts +0 -24
  114. package/src/extendStateModel.ts +0 -29
  115. /package/dist/chunks/{HelpDialog-KAG73BNB.js.map → HelpDialog-I4UK7TQB.js.map} +0 -0
  116. /package/dist/chunks/{ProteinAlignmentHelpDialog-L2GH5YTM.js.map → ProteinAlignmentHelpDialog-6VFZFN6X.js.map} +0 -0
  117. /package/dist/chunks/{molstarExports-HAGQ7LSM.js.map → molstarExports-YN6TKVEW.js.map} +0 -0
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
2
  import { observer } from 'mobx-react';
3
- import { getHighlightCoords, useStyles } from './util';
3
+ import { useStyles } from './util';
4
4
  const Highlight = observer(function Highlight({ region, model, }) {
5
5
  const { cx, classes } = useStyles();
6
- const coords = getHighlightCoords(model, region);
6
+ const coords = model.getHighlightCoords(region);
7
7
  return coords ? (React.createElement("div", { className: cx(classes.highlight, coords.width <= 3 ? classes.thinborder : undefined), style: { left: coords.left, width: coords.width } })) : null;
8
8
  });
9
9
  export default Highlight;
@@ -1,14 +1,5 @@
1
- import React from 'react';
1
+ import { addExtensionElement } from '@jbrowse/core/ui';
2
2
  import HighlightComponents from './HighlightComponents';
3
3
  export default function AddHighlightModelF(pluginManager) {
4
- pluginManager.addToExtensionPoint(
5
- // @ts-expect-error v4 hosts have no contributeToExtensionPoint
6
- 'LinearGenomeView-TracksContainerComponent',
7
- // v4 hosts seed this point with undefined rather than []
8
- (rest, { model }) => {
9
- return [
10
- ...(rest ?? []),
11
- React.createElement(HighlightComponents, { key: "highlight_protein_viewer_protein3d", model: model }),
12
- ];
13
- });
4
+ addExtensionElement(pluginManager, 'LinearGenomeView-TracksContainerComponent', HighlightComponents);
14
5
  }
@@ -1,24 +1,4 @@
1
- import { getSession } from '@jbrowse/core/util';
2
1
  import { makeStyles } from 'tss-react/mui';
3
- // Local mirror of core's getLayoutHighlightCoords / model.getHighlightCoords,
4
- // added to jbrowse after 4.3.0. Kept here so the plugin still builds against
5
- // 4.3.0; once the minimum jbrowse version ships the model method, replace the
6
- // body with `model.getHighlightCoords(region)`.
7
- export function getHighlightCoords(model, region) {
8
- const { assemblyManager } = getSession(model);
9
- const assembly = region.assemblyName
10
- ? assemblyManager.get(region.assemblyName)
11
- : undefined;
12
- const refName = assembly?.getCanonicalRefName(region.refName) ?? region.refName;
13
- const s = model.bpToPx({ refName, coord: region.start });
14
- const e = model.bpToPx({ refName, coord: region.end });
15
- return s && e
16
- ? {
17
- width: Math.max(Math.abs(e.offsetPx - s.offsetPx), 3),
18
- left: Math.min(s.offsetPx, e.offsetPx) - model.offsetPx,
19
- }
20
- : undefined;
21
- }
22
2
  export const useStyles = makeStyles()({
23
3
  highlight: {
24
4
  height: '100%',
@@ -1,9 +1,4 @@
1
- // Copied from @jbrowse/core's featureTypes: the barrel export reads undefined
2
- // on a v4 host, and the deep path is not in core's exports map.
3
- const GENE_LIKE_TYPE = /gene(_segment)?$|rna$|transcript/;
4
- export function isGeneLikeType(type) {
5
- return type !== undefined && GENE_LIKE_TYPE.test(type.toLowerCase());
6
- }
1
+ import { isGeneLikeType } from '@jbrowse/core/util';
7
2
  function isCDS(feature) {
8
3
  return feature.get('type')?.toLowerCase() === 'cds';
9
4
  }
@@ -22,12 +17,3 @@ export function codingTranscripts(feature) {
22
17
  export function isCodingFeature(feature) {
23
18
  return codingTranscripts(feature).length > 0;
24
19
  }
25
- // The outermost gene-like ancestor, so a click on an isoform opens the dialog
26
- // on the gene with every transcript to choose from, as the canvas host does.
27
- export function geneLikeRoot(feature) {
28
- let root = feature;
29
- for (let parent = root.parent?.(); parent && isGeneLikeType(parent.get('type')); parent = parent.parent?.()) {
30
- root = parent;
31
- }
32
- return root;
33
- }
@@ -70,16 +70,30 @@ const PdbSearch = observer(function PdbSearch({ feature, preferredTranscriptId,
70
70
  preferredTranscriptId,
71
71
  resetKey: uniprotId,
72
72
  });
73
- const loadingStatuses = [
74
- isLookupLoading && 'Looking up UniProt ID',
73
+ const isoformStatuses = [
75
74
  isIsoformLoading && 'Loading protein sequences from transcript isoforms',
76
75
  isRanking && 'Aligning isoforms to the structure',
76
+ ];
77
+ const lookupStatuses = [
78
+ isLookupLoading && 'Looking up UniProt ID',
77
79
  isPdbLoading && 'Listing PDB entries from PDBe',
78
- ].filter((s) => !!s);
80
+ ];
81
+ const loadingStatuses = [...lookupStatuses, ...isoformStatuses].filter((s) => !!s);
79
82
  const isLoading = loadingStatuses.length > 0;
80
83
  const error = isLoading
81
84
  ? undefined
82
85
  : (isoformError ?? lookup.lookupError ?? pdbError);
86
+ // A typed PDB ID exists to get around a lookup that is slow, failing or
87
+ // wrong, so only the isoforms it is ranked against hold its launch back
88
+ const typedIdOverrides = isPdbId(debouncedTypedPdbId);
89
+ const launchWaiting = typedIdOverrides
90
+ ? isoformStatuses.some(Boolean)
91
+ : isLoading;
92
+ const launchError = typedIdOverrides
93
+ ? launchWaiting
94
+ ? undefined
95
+ : isoformError
96
+ : error;
83
97
  return (React.createElement(React.Fragment, null,
84
98
  React.createElement(DialogContent, { className: classes.dialogContent },
85
99
  error ? React.createElement(ErrorMessage, { error: error }) : null,
@@ -113,6 +127,6 @@ const PdbSearch = observer(function PdbSearch({ feature, preferredTranscriptId,
113
127
  ". The AlphaFoldDB tab has a predicted one."))) : null,
114
128
  ranking && selectedTranscript ? (React.createElement(TranscriptSelector, { val: selectedTranscriptId, setVal: setSelectedTranscriptId, structureSequence: structureSequence, feature: feature, isoforms: transcripts, ranking: ranking })) : null),
115
129
  React.createElement(DialogActions, null,
116
- React.createElement(ProteinViewActions, { handleClose: handleClose, uniprotId: uniprotId, userSelectedProteinSequence: selectedIsoform, selectedTranscript: selectedTranscript, url: structureUrl, pdbId: selectedPdbId, feature: feature, view: view, session: session, sideBySide: sideBySide, onSideBySideChange: onSideBySideChange, isLoading: isLoading, error: error }))));
130
+ React.createElement(ProteinViewActions, { handleClose: handleClose, uniprotId: uniprotId, userSelectedProteinSequence: selectedIsoform, selectedTranscript: selectedTranscript, url: structureUrl, pdbId: selectedPdbId, feature: feature, view: view, session: session, sideBySide: sideBySide, onSideBySideChange: onSideBySideChange, isLoading: launchWaiting, error: launchError }))));
117
131
  });
118
132
  export default PdbSearch;
@@ -1,6 +1,5 @@
1
- import { getEnv } from '@jbrowse/mobx-state-tree';
2
1
  import { myfetch, uniprotGffUrl } from 'p2s_mapper';
3
- import { jexlBandColor, thresholdBandColor } from './wiggleBandColors';
2
+ import { thresholdBandColor } from './wiggleBandColors';
4
3
  import { PLDDT_BANDS } from '../../ProteinView/residueTracks';
5
4
  /**
6
5
  * Fetches UniProt GFF data and extracts unique feature types
@@ -23,7 +22,7 @@ async function fetchUniProtFeatureTypes(uniprotId) {
23
22
  function addUniProtFeatureTracks({ session, uniprotId, featureTypes, }) {
24
23
  featureTypes.forEach(type => {
25
24
  const trackId = `${uniprotId}-${type}`;
26
- session.addTrackConf({
25
+ session.addSessionTrackConf({
27
26
  type: 'FeatureTrack',
28
27
  trackId,
29
28
  name: type,
@@ -48,7 +47,7 @@ function addUniProtFeatureTracks({ session, uniprotId, featureTypes, }) {
48
47
  * Adds antigen annotation track from EBI
49
48
  */
50
49
  function addAntigenTrack({ session, uniprotId, }) {
51
- session.addTrackConf({
50
+ session.addSessionTrackConf({
52
51
  type: 'FeatureTrack',
53
52
  trackId: `${uniprotId}-Antigen`,
54
53
  name: 'Antigen',
@@ -65,7 +64,7 @@ function addAntigenTrack({ session, uniprotId, }) {
65
64
  * Adds variation track from EBI
66
65
  */
67
66
  function addVariationTrack({ session, uniprotId, }) {
68
- session.addTrackConf({
67
+ session.addSessionTrackConf({
69
68
  type: 'FeatureTrack',
70
69
  trackId: `${uniprotId}-Variation`,
71
70
  name: 'Variation',
@@ -83,7 +82,7 @@ function addVariationTrack({ session, uniprotId, }) {
83
82
  */
84
83
  function addAlphaFoldConfidenceTrack({ session, uniprotId, confidenceUrl, }) {
85
84
  if (confidenceUrl) {
86
- session.addTrackConf({
85
+ session.addSessionTrackConf({
87
86
  type: 'QuantitativeTrack',
88
87
  trackId: `${uniprotId}-AlphaFold-confidence`,
89
88
  name: 'AlphaFold confidence',
@@ -98,12 +97,6 @@ function addAlphaFoldConfidenceTrack({ session, uniprotId, confidenceUrl, }) {
98
97
  {
99
98
  type: 'LinearWiggleDisplay',
100
99
  displayId: `${uniprotId}-AlphaFold-confidence-LinearWiggleDisplay`,
101
- // Each host drops the key it does not declare: v4 reads the
102
- // renderers, v5 the display's `color`.
103
- renderers: {
104
- XYPlotRenderer: { color: jexlBandColor('score', PLDDT_BANDS) },
105
- DensityRenderer: { color: jexlBandColor('score', PLDDT_BANDS) },
106
- },
107
100
  color: thresholdBandColor('score', PLDDT_BANDS),
108
101
  },
109
102
  ],
@@ -113,8 +106,8 @@ function addAlphaFoldConfidenceTrack({ session, uniprotId, confidenceUrl, }) {
113
106
  /**
114
107
  * Adds AlphaMissense pathogenicity scores track
115
108
  */
116
- function addAlphaMissenseTrack({ session, uniprotId, hasMultiWiggleDisplay, }) {
117
- session.addTrackConf({
109
+ function addAlphaMissenseTrack({ session, uniprotId, }) {
110
+ session.addSessionTrackConf({
118
111
  type: 'MultiQuantitativeTrack',
119
112
  trackId: `${uniprotId}-AlphaMissense-scores`,
120
113
  name: 'AlphaMissense scores',
@@ -125,28 +118,18 @@ function addAlphaMissenseTrack({ session, uniprotId, hasMultiWiggleDisplay, }) {
125
118
  uri: `https://alphafold.ebi.ac.uk/files/AF-${uniprotId}-F1-aa-substitutions.csv`,
126
119
  },
127
120
  },
128
- // v5 folded MultiLinearWiggleDisplay into LinearWiggleDisplay and warns on
129
- // a config naming the old type. v4's density renderer ramps from white to
130
- // posColor, so pathogenic reads red; v5 diverges at AlphaMissense's 0.5.
131
121
  displays: [
132
- hasMultiWiggleDisplay
133
- ? {
134
- type: 'MultiLinearWiggleDisplay',
135
- displayId: `${uniprotId}-AlphaMissense-scores-MultiLinearWiggleDisplay`,
136
- defaultRendering: 'multirowdensity',
137
- renderers: { MultiDensityRenderer: { posColor: '#d7191c' } },
138
- }
139
- : {
140
- type: 'LinearWiggleDisplay',
141
- displayId: `${uniprotId}-AlphaMissense-scores-LinearWiggleDisplay`,
142
- defaultRendering: 'density',
143
- color: {
144
- field: 'score',
145
- scale: 'linear',
146
- range: ['#2c7bb6', '#ffffff', '#d7191c'],
147
- domainMid: 0.5,
148
- },
122
+ {
123
+ type: 'LinearWiggleDisplay',
124
+ displayId: `${uniprotId}-AlphaMissense-scores-LinearWiggleDisplay`,
125
+ defaultRendering: 'density',
126
+ color: {
127
+ field: 'score',
128
+ scale: 'linear',
129
+ range: ['#2c7bb6', '#ffffff', '#d7191c'],
130
+ domainMid: 0.5,
149
131
  },
132
+ },
150
133
  ],
151
134
  });
152
135
  }
@@ -178,9 +161,6 @@ export async function addAllProteinTracks({ session, uniprotId, confidenceUrl, }
178
161
  addAlphaMissenseTrack({
179
162
  session,
180
163
  uniprotId,
181
- hasMultiWiggleDisplay: getEnv(session)
182
- .pluginManager.getDisplayElements()
183
- .some(d => d.name === 'MultiLinearWiggleDisplay'),
184
164
  });
185
165
  }
186
166
  }
@@ -1,21 +1,7 @@
1
1
  /**
2
- * A v4 wiggle renderer's `color` callback painting `field` by band. v4 hosts
3
- * read colour only from the renderer, and only a callback can place a cut
4
- * anywhere but 0 on v4.3.0, which drops a configured pivot on its way to the
5
- * worker. v4 also evaluates it once with no feature, and a throw there leaves
6
- * the track an error message, hence the guard.
7
- */
8
- export function jexlBandColor(field, bands) {
9
- const value = `get(feature,'${field}')`;
10
- const body = bands.reduceRight((rest, band) => band.upTo === Infinity
11
- ? `'${band.color}'`
12
- : `(${value}<=${band.upTo}?'${band.color}':${rest})`, "'#cccccc'");
13
- return `jexl:feature?${body}:'#cccccc'`;
14
- }
15
- /**
16
- * The same bands as the display-level threshold scale v5 hosts read. That
17
- * scale puts a value equal to a cut in the upper band, so each cut moves just
18
- * past its bound to keep the bound in its own band, as `bandColor` does.
2
+ * The display-level threshold scale puts a value equal to a cut in the upper
3
+ * band, so each cut moves just past its bound to keep the bound in its own
4
+ * band, as `bandColor` does.
19
5
  */
20
6
  export function thresholdBandColor(field, bands) {
21
7
  return {
@@ -7,8 +7,11 @@ import useIsoformProteinSequences from './useIsoformProteinSequences';
7
7
  export default function useAlphaFoldData({ uniprotId, feature, view, }) {
8
8
  const { data, isLoading, isValidating, error } = useSWR(uniprotId ? ['alphafold-models', uniprotId] : null, ([, id]) => fetchAlphaFoldModels(id), { ...STATIC_SWR_OPTIONS, keepPreviousData: true });
9
9
  const { isoformSequences } = useIsoformProteinSequences({ feature, view });
10
- // with an error, data is the previous accession's (keepPreviousData)
11
- const model = useMemo(() => data && !error ? pickAlphaFoldModel(data, isoformSequences) : undefined, [data, error, isoformSequences]);
10
+ // keepPreviousData hands back the last accession's models after an error
11
+ // and after the accession is cleared, when the key is null
12
+ const model = useMemo(() => uniprotId && data && !error
13
+ ? pickAlphaFoldModel(data, isoformSequences)
14
+ : undefined, [uniprotId, data, error, isoformSequences]);
12
15
  return {
13
16
  isLoading,
14
17
  isValidating,
@@ -17,10 +17,16 @@ export function describeOrganism(taxonId, source) {
17
17
  * to every tab, so all of them run one search and agree on what the gene is.
18
18
  */
19
19
  export default function useUniProtIdLookup({ feature, view, }) {
20
- const [lookupMode, setLookupMode] = useState('auto');
20
+ const geneIds = extractFeatureIdentifiers(feature);
21
+ const featureUniprotId = geneIds.uniprotId;
22
+ const hasSearchableIdentifier = geneIds.recognizedIds.length > 0 || !!geneIds.geneName;
23
+ // Nothing to search and no accession on the feature: the auto mode has no
24
+ // query to run, so the dialog opens on the manual field instead of reporting
25
+ // an empty result for an empty query. Only the opening mode is chosen here;
26
+ // deciding it on every render snapped the user's pick back.
27
+ const [lookupMode, setLookupMode] = useState(() => featureUniprotId ? 'feature' : hasSearchableIdentifier ? 'auto' : 'manual');
21
28
  const [manualUniprotId, setManualUniprotId] = useState('');
22
29
  const [taxonIdInput, setTaxonIdInput] = useState('');
23
- const geneIds = extractFeatureIdentifiers(feature);
24
30
  // The gene-name UniProt search is ambiguous across species, so scope it to
25
31
  // the assembly's organism where the assembly says what that is. jb2hubs
26
32
  // assemblies carry the NCBI taxon in the reference-sequence track metadata
@@ -46,17 +52,7 @@ export default function useUniProtIdLookup({ feature, view, }) {
46
52
  const taxonIdError = typedTaxon !== '' && !hasOverride;
47
53
  const [selectedQueryId, setSelectedQueryId] = useState('auto');
48
54
  const [selectedUniprotId, setSelectedUniprotId] = useState();
49
- const featureUniprotId = geneIds.uniprotId;
50
- const hasSearchableIdentifier = geneIds.recognizedIds.length > 0 || !!geneIds.geneName;
51
- // Nothing to search and no accession on the feature: the auto mode has no
52
- // query to run, so the dialog opens on the manual field instead of reporting
53
- // an empty result for an empty query.
54
- const effectiveLookupMode = lookupMode === 'auto' && featureUniprotId
55
- ? 'feature'
56
- : lookupMode === 'auto' && !hasSearchableIdentifier
57
- ? 'manual'
58
- : lookupMode;
59
- const isAutoMode = effectiveLookupMode === 'auto';
55
+ const isAutoMode = lookupMode === 'auto';
60
56
  const { entries: uniprotEntries, isLoading: isLookupLoading, error: lookupError, partialFailure: lookupPartialFailure, } = useUniProtSearch({
61
57
  recognizedIds: geneIds.recognizedIds,
62
58
  geneId: geneIds.geneId,
@@ -74,13 +70,13 @@ export default function useUniProtIdLookup({ feature, view, }) {
74
70
  ? selectedUniprotId
75
71
  : undefined;
76
72
  const autoUniprotId = uniprotEntries[0]?.accession;
77
- const uniprotId = effectiveLookupMode === 'feature'
73
+ const uniprotId = lookupMode === 'feature'
78
74
  ? featureUniprotId
79
75
  : isAutoMode
80
76
  ? (pickedUniprotId ?? autoUniprotId)
81
77
  : debouncedManualUniprotId;
82
78
  return {
83
- lookupMode: effectiveLookupMode,
79
+ lookupMode,
84
80
  setLookupMode,
85
81
  manualUniprotId,
86
82
  setManualUniprotId,
@@ -1,12 +1,15 @@
1
- import { getContainingTrack, getSession } from '@jbrowse/core/util';
1
+ import { getContainingTrack, getSession, isGeneLikeType, } from '@jbrowse/core/util';
2
2
  import AddIcon from '@mui/icons-material/Add';
3
- import { extendPluggableStateModel } from '../extendStateModel';
4
- import { geneLikeRoot, isCodingFeature, isGeneLikeType } from './codingFeature';
3
+ import { isCodingFeature } from './codingFeature';
5
4
  import LaunchProteinViewDialog from './components/LaunchProteinViewDialog';
6
5
  function isDisplay(elt) {
7
6
  return elt.name === 'LinearBasicDisplay';
8
7
  }
9
- function canvasTarget(info, fetchFullFeature) {
8
+ export function resolveTarget(self) {
9
+ const info = self.contextMenuInfo;
10
+ if (!info) {
11
+ return undefined;
12
+ }
10
13
  const { item, subfeature, displayedRegionIndex } = info;
11
14
  const type = subfeature ? subfeature.type : item.type;
12
15
  // The parent gene, not the clicked isoform: the dialog picks the transcript
@@ -16,32 +19,10 @@ function canvasTarget(info, fetchFullFeature) {
16
19
  ? undefined
17
20
  : {
18
21
  type,
19
- fetchFeature: () => fetchFullFeature(parentId, displayedRegionIndex),
22
+ fetchFeature: () => self.fetchFullFeature(parentId, displayedRegionIndex),
20
23
  preferredTranscriptId: subfeature?.featureId,
21
24
  };
22
25
  }
23
- // The hit test only carries a type, so a canvas host learns whether the gene
24
- // codes for anything after the fetch; a legacy host has the whole feature and
25
- // can decline up front.
26
- function legacyTarget(feature) {
27
- const root = geneLikeRoot(feature);
28
- const type = root.get('type');
29
- return type === undefined || !isCodingFeature(root)
30
- ? undefined
31
- : {
32
- type,
33
- feature: root,
34
- preferredTranscriptId: root === feature ? undefined : feature.id(),
35
- };
36
- }
37
- export function resolveTarget(self) {
38
- const { contextMenuFeature, contextMenuInfo, fetchFullFeature } = self;
39
- return contextMenuInfo && fetchFullFeature
40
- ? canvasTarget(contextMenuInfo, fetchFullFeature)
41
- : contextMenuFeature
42
- ? legacyTarget(contextMenuFeature)
43
- : undefined;
44
- }
45
26
  function launchProteinView(self, target) {
46
27
  const track = getContainingTrack(self);
47
28
  const session = getSession(track);
@@ -52,41 +33,32 @@ function launchProteinView(self, target) {
52
33
  { model: track, handleClose, feature, preferredTranscriptId },
53
34
  ]);
54
35
  };
55
- if ('feature' in target) {
56
- openDialog(target.feature);
57
- }
58
- else {
59
- target
60
- .fetchFeature()
61
- .then(feature => {
62
- if (!feature) {
63
- session.notify('Could not load feature for protein view', 'warning');
64
- }
65
- else if (!isCodingFeature(feature)) {
66
- session.notify(`${feature.get('name') ?? feature.get('id') ?? 'This feature'} has no coding sequence, so there is no protein to show`, 'info');
67
- }
68
- else {
69
- openDialog(feature);
70
- }
71
- })
72
- .catch((e) => {
73
- console.error(e);
74
- session.notifyError(`${e}`, e);
75
- });
76
- }
36
+ target
37
+ .fetchFeature()
38
+ .then(feature => {
39
+ if (!feature) {
40
+ session.notify('Could not load feature for protein view', 'warning');
41
+ }
42
+ else if (!isCodingFeature(feature)) {
43
+ session.notify(`${feature.get('name') ?? feature.get('id') ?? 'This feature'} has no coding sequence, so there is no protein to show`, 'info');
44
+ }
45
+ else {
46
+ openDialog(feature);
47
+ }
48
+ })
49
+ .catch((e) => {
50
+ console.error(e);
51
+ session.notifyError(`${e}`, e);
52
+ });
77
53
  }
78
54
  function extendStateModel(stateModel) {
79
55
  return stateModel.views((self) => {
80
- // .call(self), not a bare call: the canvas display's own contextMenuItems
81
- // reads `this.isGeneLike`, so invoking it detached throws on undefined and
82
- // the ErrorBoundary around the menu swallows it -- the user right-clicks a
83
- // feature and gets no menu at all, not merely no protein item.
84
56
  const superContextMenuItems = self.contextMenuItems;
85
57
  return {
86
58
  contextMenuItems() {
87
59
  const target = resolveTarget(self);
88
60
  return [
89
- ...superContextMenuItems.call(self),
61
+ ...superContextMenuItems(),
90
62
  ...(target && isGeneLikeType(target.type)
91
63
  ? [
92
64
  {
@@ -106,7 +78,7 @@ function extendStateModel(stateModel) {
106
78
  export default function LaunchProteinViewF(pluginManager) {
107
79
  pluginManager.addToExtensionPoint('Core-extendPluggableElement', (elt) => {
108
80
  if (isDisplay(elt)) {
109
- extendPluggableStateModel(elt, extendStateModel);
81
+ elt.extendStateModel(extendStateModel);
110
82
  }
111
83
  return elt;
112
84
  });
@@ -69,11 +69,35 @@ export async function submitFoldseekSearch({ aaSequence, di3Sequence, databases,
69
69
  if (!response.ok) {
70
70
  throw await httpError(response, url);
71
71
  }
72
- // Read the body as text rather than response.json() so a non-JSON error page
73
- // (a gateway 500's HTML) surfaces as itself instead of an opaque
74
- // SyntaxError.
72
+ // The server answers a refusal (RATELIMIT, MAINTENANCE) with a 200 and no
73
+ // id, which used to be polled as ticket "undefined" for three minutes
75
74
  const text = await response.text();
76
- return JSON.parse(text);
75
+ const ticket = parseTicket(text);
76
+ if (!ticket) {
77
+ throw new Error(`Foldseek did not accept the search: ${text.slice(0, 200)}`);
78
+ }
79
+ return ticket;
80
+ }
81
+ function parseTicket(text) {
82
+ let body;
83
+ try {
84
+ body = JSON.parse(text);
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ if (typeof body !== 'object' || body === null) {
90
+ return undefined;
91
+ }
92
+ const id = Reflect.get(body, 'id');
93
+ const status = Reflect.get(body, 'status');
94
+ return typeof id === 'string' &&
95
+ (status === 'PENDING' ||
96
+ status === 'RUNNING' ||
97
+ status === 'COMPLETE' ||
98
+ status === 'ERROR')
99
+ ? { id, status }
100
+ : undefined;
77
101
  }
78
102
  async function pollFoldseekStatus({ ticketId, signal, }) {
79
103
  // Use the /tickets endpoint (plural) with POST
@@ -114,10 +138,6 @@ export async function waitForFoldseekResults({ ticketId, onStatusChange, signal,
114
138
  throw abortError(signal);
115
139
  }
116
140
  const status = await pollFoldseekStatus({ ticketId, signal });
117
- if (status.status === 'ERROR') {
118
- console.error('[Foldseek] Search error:', status);
119
- throw new Error(`Foldseek search failed: ${status.error ?? 'Unknown error'}`);
120
- }
121
141
  if (status.status === 'COMPLETE') {
122
142
  onStatusChange?.('Fetching results...');
123
143
  const apiResponse = await getFoldseekResults({ ticketId, signal });
@@ -131,6 +151,11 @@ export async function waitForFoldseekResults({ ticketId, onStatusChange, signal,
131
151
  };
132
152
  return results;
133
153
  }
154
+ // the server also answers RATELIMIT, MAINTENANCE and UNKNOWN, none of
155
+ // which a wait resolves
156
+ if (status.status !== 'PENDING' && status.status !== 'RUNNING') {
157
+ throw new Error(`Foldseek search failed: ${status.error ?? status.status}`);
158
+ }
134
159
  onStatusChange?.(`Search ${status.status.toLowerCase()}... (${elapsedSeconds()}s)`);
135
160
  await timeout(1000, signal);
136
161
  }
@@ -22,9 +22,9 @@ export async function safeLaunch(fn, onSuccess, onError) {
22
22
  * A launch that names its own structure needs no accession. Bypassing a lookup
23
23
  * that failed or resolved the wrong gene is the whole point of typing a PDB id,
24
24
  * and the view resolves SIFTS from the entry itself; the accession only feeds
25
- * the feature tracks and the view's name, both of which do without it. The
26
- * AlphaFold tab keeps the requirement for free — its structure url is derived
27
- * from the accession, so no accession means no structure either.
25
+ * the feature tracks and the view's name, both of which do without it. On the
26
+ * AlphaFold tab the url is derived from the accession, so no accession means
27
+ * no url either; `useAlphaFoldData` makes sure of it.
28
28
  */
29
29
  export function getLaunchMissingReasons({ uniprotId, userSelectedProteinSequence, selectedTranscript, url, pdbId, }) {
30
30
  const namesOwnStructure = !!url || !!pdbId;
@@ -1,4 +1,4 @@
1
- import { isSessionWithAddTracks } from './sessionWithAddTracks';
1
+ import { isSessionWithAddSessionTrack } from '@jbrowse/core/util';
2
2
  import { maybeLaunchSideBySide } from './sideBySide';
3
3
  import { getGeneDisplayName, getTranscriptDisplayName } from './util';
4
4
  import { proteinViewSnapshot } from '../../ProteinView/proteinViewSpec';
@@ -35,7 +35,7 @@ export function launch3DProteinView({ session, view, feature, selectedTranscript
35
35
  return proteinView;
36
36
  }
37
37
  // The 1D annotation view adds temporary tracks/assemblies, so it requires a
38
- // SessionWithAddTracks and a known uniprotId. Demanding both in the signature
38
+ // SessionWithAddSessionTrack and a known uniprotId. Demanding both in the signature
39
39
  // forces callers to narrow up front — there's no silent no-op when a wide
40
40
  // session or missing id slips through.
41
41
  async function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }) {
@@ -63,7 +63,9 @@ export const PROTEIN_LAUNCH_LABELS = {
63
63
  // single source of truth — an unavailable action is unrepresentable rather than
64
64
  // a menu item that silently no-ops.
65
65
  export function getConditionalProteinLaunches({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }) {
66
- const addTracksSession = isSessionWithAddTracks(session) ? session : undefined;
66
+ const addTracksSession = isSessionWithAddSessionTrack(session)
67
+ ? session
68
+ : undefined;
67
69
  return {
68
70
  launch1D: addTracksSession && uniprotId
69
71
  ? () => launch1DProteinView({
@@ -21,39 +21,16 @@ let warnedPartial = false;
21
21
  function isSessionWithWorkspaces(session) {
22
22
  const canEnable = hasAction(session, 'setUseWorkspaces');
23
23
  const canPlace = hasAction(session, 'setPendingMove');
24
- // Missing BOTH is an embedded session: it has no workspaces, there is nothing
25
- // to ask for, and silence is the right answer.
26
- //
27
- // Missing ONE is a host that has workspaces but places views some other way —
28
- // and silence there is how this broke before. jbrowse-web folded
29
- // `setPendingMove` into its layout `init`, this guard went false, and the
30
- // plugin simply stopped asking for the split: no error, no missing feature,
31
- // just two views quietly stacking. Nobody noticed for weeks. Feature
32
- // detection cannot ask the host to announce a change, but it can tell "not
33
- // supported here" from "supported, and gone".
34
- //
35
- // Two very different hosts produce this one shape, and nothing on the session
36
- // tells them apart, so the message carries both rather than the guess:
37
- //
38
- // - Releases through v4.3.0, where placement is `setPendingMoveToSplitRight`,
39
- // a module function in @jbrowse/app-core's DockviewContext, not a session
40
- // action. Nothing is wrong and nothing needs fixing; this plugin targets v5+
41
- // and does not reach for v4's door. Measured 2026-08-17: v4.3.0 and latest
42
- // stack, main splits.
43
- // - A newer host that moved the action out from under us, which is the
44
- // regression this warning exists to catch.
45
- //
46
- // Do not quiet the first case by sniffing the version. The alarm is only worth
47
- // having if it fires on a shape it cannot explain, and these two shapes are
48
- // identical.
24
+ // Missing BOTH is an embedded session, with no workspaces to ask for. Missing
25
+ // ONE means the host moved the action out from under us: jbrowse-web once
26
+ // folded `setPendingMove` into its layout `init`, and the views quietly
27
+ // stacked for weeks, so that shape warns.
49
28
  if (canEnable !== canPlace && !warnedPartial) {
50
29
  warnedPartial = true;
51
30
  console.warn(`jbrowse-plugin-protein3d: this session supports workspaces but not ` +
52
31
  `${canPlace ? 'setUseWorkspaces' : 'setPendingMove'}, so the ` +
53
- `side-by-side launch was skipped and the views will stack. Expected on ` +
54
- `releases through v4.3.0, which place views through @jbrowse/app-core ` +
55
- `instead; on a newer host it means the session API moved and the plugin ` +
56
- `needs updating to match.`);
32
+ `side-by-side launch was skipped and the views will stack. The session ` +
33
+ `API moved and the plugin needs updating to match.`);
57
34
  }
58
35
  return canEnable && canPlace;
59
36
  }
@@ -16,11 +16,8 @@ export async function fetchRegionSequence({ session, assemblyName, refName, star
16
16
  if (!assembly) {
17
17
  throw new Error('assembly not found');
18
18
  }
19
- const sessionId = 'getSequence';
20
- // a named object keeps sessionId, which v4 hosts read from the args
21
- const args = {
19
+ const [feat] = await rpcManager.call('getSequence', 'CoreGetFeatures', {
22
20
  adapterConfig: getConf(assembly, ['sequence', 'adapter']),
23
- sessionId,
24
21
  regions: [
25
22
  {
26
23
  start,
@@ -29,10 +26,9 @@ export async function fetchRegionSequence({ session, assemblyName, refName, star
29
26
  assemblyName: assembly.name,
30
27
  },
31
28
  ],
32
- };
33
- const [feat] = await rpcManager.call(sessionId, 'CoreGetFeatures', args);
29
+ });
34
30
  const seq = feat?.get('seq');
35
- return { seq, assemblyGeneticCodeId: assemblyGeneticCode(assembly, refName) };
31
+ return { seq, assemblyGeneticCodeId: assembly.getGeneticCodeId(refName) };
36
32
  }
37
33
  export async function fetchProteinSeq({ feature, session, assemblyName, }) {
38
34
  const { seq, assemblyGeneticCodeId } = await fetchRegionSequence({
@@ -85,8 +81,3 @@ export async function fetchTranscriptProteinSeqs({ transcripts, session, assembl
85
81
  fetchSpan: span => fetchRegionSequence({ session, assemblyName, ...span }),
86
82
  });
87
83
  }
88
- // v5 hosts only; a v4 assembly has neither the method nor the config slot
89
- function assemblyGeneticCode(assembly, refName) {
90
- const { getGeneticCodeId } = assembly;
91
- return getGeneticCodeId?.call(assembly, refName);
92
- }