jbrowse-plugin-msaview 3.6.0 → 3.7.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 (149) hide show
  1. package/README.md +46 -24
  2. package/dist/LaunchMsaView/cleanProteinSequence.test.d.ts +1 -0
  3. package/dist/LaunchMsaView/cleanProteinSequence.test.js +77 -0
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -32
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +10 -15
  6. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +14 -13
  7. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +25 -22
  8. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +14 -19
  9. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +2 -1
  10. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +18 -2
  11. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.d.ts +1 -0
  12. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.js +13 -0
  13. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +19 -14
  14. package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
  15. package/dist/LaunchMsaView/components/SubmitCancelActions.js +10 -7
  16. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +20 -2
  17. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +8 -1
  18. package/dist/LaunchMsaView/components/calculateProteinSequence.js +25 -20
  19. package/dist/LaunchMsaView/components/launchConnectedView.d.ts +23 -0
  20. package/dist/LaunchMsaView/components/launchConnectedView.js +39 -0
  21. package/dist/LaunchMsaView/detectQueryRow.js +4 -7
  22. package/dist/LaunchMsaView/util.d.ts +6 -0
  23. package/dist/LaunchMsaView/util.js +7 -1
  24. package/dist/LaunchMsaViewExtensionPoint/index.js +25 -2
  25. package/dist/LaunchMsaViewExtensionPoint/index.test.js +60 -0
  26. package/dist/MsaViewPanel/afterCreateAutoruns.js +12 -3
  27. package/dist/MsaViewPanel/doLaunchBlast.js +2 -1
  28. package/dist/MsaViewPanel/doLaunchBlast.test.js +12 -0
  29. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
  30. package/dist/MsaViewPanel/genomeToMSA.js +3 -3
  31. package/dist/MsaViewPanel/genomeToMSA.test.js +69 -23
  32. package/dist/MsaViewPanel/launchAutoruns.test.d.ts +1 -0
  33. package/dist/MsaViewPanel/launchAutoruns.test.js +36 -0
  34. package/dist/MsaViewPanel/model.d.ts +16 -13
  35. package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
  36. package/dist/MsaViewPanel/msaDataStore.js +3 -12
  37. package/dist/MsaViewPanel/msaDataStore.test.js +8 -1
  38. package/dist/MsaViewPanel/observeProteinHighlights.test.js +4 -4
  39. package/dist/MsaViewPanel/regionSnapshot.test.d.ts +1 -0
  40. package/dist/MsaViewPanel/regionSnapshot.test.js +29 -0
  41. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +41 -4
  42. package/dist/MsaViewPanel/resolveConnectedTranscript.js +103 -68
  43. package/dist/MsaViewPanel/resolveConnectedTranscript.test.d.ts +1 -0
  44. package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +149 -0
  45. package/dist/MsaViewPanel/runLaunch.d.ts +8 -4
  46. package/dist/MsaViewPanel/runLaunch.js +15 -4
  47. package/dist/MsaViewPanel/runLaunch.test.js +16 -0
  48. package/dist/MsaViewPanel/storedData.test.js +23 -0
  49. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +3 -2
  50. package/dist/MsaViewPanel/util.d.ts +23 -23
  51. package/dist/MsaViewPanel/util.js +23 -24
  52. package/dist/index.js +17 -10
  53. package/dist/jbrowse-plugin-msaview.umd.production.min.js +38 -36
  54. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  55. package/dist/utils/blastCache.d.ts +26 -2
  56. package/dist/utils/blastCache.js +51 -39
  57. package/dist/utils/blastCache.test.js +46 -19
  58. package/dist/utils/domainCache.d.ts +1 -1
  59. package/dist/utils/domainCache.js +18 -14
  60. package/dist/utils/ebiJobDispatcher.d.ts +11 -0
  61. package/dist/utils/ebiJobDispatcher.js +12 -13
  62. package/dist/utils/ebiJobDispatcher.test.js +17 -3
  63. package/dist/utils/eutils.d.ts +14 -0
  64. package/dist/utils/eutils.js +48 -14
  65. package/dist/utils/eutils.test.d.ts +1 -0
  66. package/dist/utils/eutils.test.js +31 -0
  67. package/dist/utils/idb.d.ts +16 -3
  68. package/dist/utils/idb.js +51 -5
  69. package/dist/utils/idb.test.d.ts +1 -0
  70. package/dist/utils/idb.test.js +78 -0
  71. package/dist/utils/ncbiDomains.js +4 -4
  72. package/dist/utils/ncbiOrthologs.d.ts +1 -1
  73. package/dist/utils/ncbiOrthologs.js +18 -17
  74. package/dist/utils/ncbiOrthologs.test.js +17 -1
  75. package/dist/utils/ncbiTaxonomy.js +14 -15
  76. package/dist/utils/taxonomyNames.js +23 -19
  77. package/dist/utils/taxonomyNames.test.d.ts +1 -0
  78. package/dist/utils/taxonomyNames.test.js +33 -0
  79. package/dist/utils/unirefHomologs.js +3 -3
  80. package/dist/utils/unirefHomologs.test.js +8 -1
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.js +1 -1
  83. package/package.json +11 -10
  84. package/src/LaunchMsaView/cleanProteinSequence.test.ts +91 -0
  85. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +35 -32
  86. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +10 -14
  87. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +16 -15
  88. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +34 -21
  89. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +18 -21
  90. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.ts +16 -0
  91. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +24 -2
  92. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +19 -13
  93. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +27 -2
  94. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +12 -7
  95. package/src/LaunchMsaView/components/calculateProteinSequence.ts +28 -18
  96. package/src/LaunchMsaView/components/launchConnectedView.ts +56 -0
  97. package/src/LaunchMsaView/detectQueryRow.ts +5 -7
  98. package/src/LaunchMsaView/util.ts +7 -1
  99. package/src/LaunchMsaViewExtensionPoint/index.test.ts +64 -0
  100. package/src/LaunchMsaViewExtensionPoint/index.ts +68 -5
  101. package/src/MsaViewPanel/afterCreateAutoruns.ts +12 -3
  102. package/src/MsaViewPanel/doLaunchBlast.test.ts +20 -1
  103. package/src/MsaViewPanel/doLaunchBlast.ts +2 -1
  104. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +1 -1
  105. package/src/MsaViewPanel/genomeToMSA.test.ts +75 -23
  106. package/src/MsaViewPanel/genomeToMSA.ts +3 -2
  107. package/src/MsaViewPanel/launchAutoruns.test.ts +45 -0
  108. package/src/MsaViewPanel/msaDataStore.test.ts +14 -2
  109. package/src/MsaViewPanel/msaDataStore.ts +4 -12
  110. package/src/MsaViewPanel/observeProteinHighlights.test.ts +4 -4
  111. package/src/MsaViewPanel/regionSnapshot.test.ts +34 -0
  112. package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +183 -0
  113. package/src/MsaViewPanel/resolveConnectedTranscript.ts +154 -71
  114. package/src/MsaViewPanel/runLaunch.test.ts +19 -0
  115. package/src/MsaViewPanel/runLaunch.ts +21 -4
  116. package/src/MsaViewPanel/storedData.test.ts +32 -0
  117. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +3 -2
  118. package/src/MsaViewPanel/util.ts +38 -29
  119. package/src/index.ts +19 -10
  120. package/src/utils/blastCache.test.ts +59 -20
  121. package/src/utils/blastCache.ts +62 -39
  122. package/src/utils/domainCache.ts +27 -15
  123. package/src/utils/ebiJobDispatcher.test.ts +18 -3
  124. package/src/utils/ebiJobDispatcher.ts +12 -16
  125. package/src/utils/eutils.test.ts +39 -0
  126. package/src/utils/eutils.ts +64 -14
  127. package/src/utils/idb.test.ts +106 -0
  128. package/src/utils/idb.ts +57 -5
  129. package/src/utils/ncbiDomains.ts +4 -4
  130. package/src/utils/ncbiOrthologs.test.ts +20 -0
  131. package/src/utils/ncbiOrthologs.ts +26 -28
  132. package/src/utils/ncbiTaxonomy.ts +17 -20
  133. package/src/utils/taxonomyNames.test.ts +41 -0
  134. package/src/utils/taxonomyNames.ts +33 -19
  135. package/src/utils/unirefHomologs.test.ts +11 -1
  136. package/src/utils/unirefHomologs.ts +3 -3
  137. package/src/version.ts +1 -1
  138. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.d.ts +0 -16
  139. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +0 -31
  140. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +0 -16
  141. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +0 -16
  142. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +0 -9
  143. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +0 -15
  144. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +0 -13
  145. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +0 -22
  146. package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +0 -59
  147. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +0 -43
  148. package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +0 -31
  149. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +0 -42
package/README.md CHANGED
@@ -1,19 +1,28 @@
1
1
  # jbrowse-plugin-msaview
2
2
 
3
- This plugin packages https://github.com/gmod/react-msaview for usage inside of
4
- JBrowse 2
5
-
6
- See the docs for the react-msaview for more info
7
- https://github.com/GMOD/react-msaview/blob/main/docs/user_guide.md
3
+ A multiple sequence alignment and phylogenetic tree viewer for JBrowse 2, linked
4
+ column by column to the genome. It packages
5
+ [react-msaview](https://github.com/GMOD/react-msaview), whose
6
+ [user guide](https://github.com/GMOD/react-msaview/blob/main/docs/user_guide.md)
7
+ covers the viewer itself.
8
8
 
9
9
  ## Gallery
10
10
 
11
11
  ![](img/1.png)
12
12
 
13
- MSAView plugin running in JBrowse 2
13
+ The [demo](#demo) below: TP53 in the genome view beside a vertebrate p53
14
+ alignment carrying per-residue variant tracks. Each release recaptures it with
15
+ `pnpm readme-figure`.
14
16
 
15
17
  ## Demo
16
18
 
19
+ [p53 across vertebrates, linked to the genome and the structure](https://jbrowse.org/code/jb2/main/?config=https://jbrowse.org/ucsc/hg38/config.json&session=spec-{%22views%22:[{%22type%22:%22LinearGenomeView%22,%22id%22:%22lgv1%22,%22assembly%22:%22hg38%22,%22loc%22:%22chr17:7,661,779-7,687,538%22,%22tracks%22:[%22hg38-ncbiRefSeqSelect%22]},{%22type%22:%22MsaView%22,%22displayName%22:%22p53%20across%20vertebrates%22,%22msa%22:%22https://gmod.org/JBrowseMSA/demo/data/p53/p53-vertebrates.afa%22,%22tree%22:%22https://gmod.org/JBrowseMSA/demo/data/p53/p53-vertebrates.nh%22,%22query%22:%22Human%22,%22connectedViewId%22:%22lgv1%22,%22connectedTranscript%22:%22NM_000546.6%22,%22placement%22:%22splitRight%22,%22colorSchemeName%22:%22clustalx_protein_dynamic%22,%22highlights%22:[{%22start%22:102,%22end%22:292,%22label%22:%22DNA-binding%22,%22color%22:%22rgba%28255,140,0,0.15%29%22},175,245,248,249,273,282],%22region%22:%22170-290%22,%22columnTracks%22:[{%22name%22:%22ClinVar%20pathogenic%20missense%22,%22color%22:%22%23c0392b%22,%22max%22:8,%22start%22:105,%22values%22:[2,1,0,0,2,4,3,0,6,0,0,0,0,0,0,0,0,0,0,1,2,1,5,0,0,2,2,3,1,1,4,0,0,2,0,0,2,0,3,0,0,0,1,0,0,0,6,2,0,0,1,1,3,6,2,0,1,0,2,1,1,0,0,2,0,0,2,1,3,0,3,3,1,3,5,1,3,0,0,0,0,0,0,0,0,1,0,0,4,3,1,1,1,0,1,0,0,0,0,0,3,0,0,0,0,0,0,0,3,2,1,1,0,1,0,3,0,0,0,0,0,0,0,0,0,0,0,3,0,4,0,2,4,6,2,3,5,2,0,4,5,4,1,6,2,1,2,0,1,3,0,0,0,2,1,0,0,0,0,0,2,3,3,0,1,4,1,3,6,0,2,1,1,3,0,3,8,3,1,0,2,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,5,0,0,0,1,1,0,2,0,0,1]},{%22name%22:%22AlphaMissense%20mean%20%28x100%29%22,%22color%22:%22%231565c0%22,%22max%22:100,%22values%22:[49,27,19,11,12,14,18,10,12,14,28,17,40,57,73,65,65,68,95,29,30,72,95,21,24,60,39,21,18,17,18,16,17,12,11,13,14,13,14,21,22,20,17,20,16,16,15,14,17,17,15,13,37,29,11,19,16,11,11,12,14,20,14,12,11,20,13,19,15,14,12,13,15,17,14,17,15,19,20,17,15,15,22,22,19,22,20,24,17,23,67,41,46,66,63,37,90,98,63,35,36,20,76,15,99,13,58,48,98,26,81,57,97,43,17,64,87,68,90,98,96,92,73,81,98,91,95,23,13,81,56,97,84,97,98,87,86,81,91,84,94,93,89,66,81,57,77,26,20,13,90,69,17,52,70,49,91,98,87,88,94,87,95,89,48,54,52,76,76,68,92,87,98,78,99,100,98,97,100,96,85,50,38,52,32,74,52,32,83,87,71,47,99,95,89,95,91,96,95,81,20,25,69,48,98,24,64,93,17,23,84,56,99,92,91,93,70,89,76,91,77,27,92,87,57,92,58,35,54,68,75,87,85,87,74,93,96,100,95,98,99,100,94,100,100,98,97,100,99,93,93,59,91,91,87,92,93,99,87,22,21,94,24,76,83,99,98,36,61,97,93,93,99,95,100,97,100,100,99,100,100,97,84,60,98,96,47,62,13,18,56,34,17,17,11,9,9,16,11,15,13,18,20,17,81,75,26,26,23,13,12,14,16,18,18,14,15,15,47,54,46,19,13,34,24,50,56,88,36,87,48,93,63,98,73,40,89,90,45,60,88,51,36,81,77,55,79,75,87,31,54,67,23,18,17,16,25,22,13,15,15,16,20,18,15,20,25,13,15,56,21,47,45,21,16,16,15,21,16,20,66,47,13,26,19,66,13,28,18,16,47,54,66]},{%22name%22:%22MaveDB%20nutlin-3,%20p53WT%22,%22color%22:%22%232e7d32%22,%22max%22:2,%22start%22:27,%22values%22:[0.6,0,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.1,0,0,0.2,0.4,0,0,0,0,0.1,0,0.8,0,0.7,0.3,1.6,0.1,1.4,0.1,1.3,0,0,0,0.1,0,0.1,0.5,0.2,0.4,0.4,0.8,0.8,1.4,1.5,0.1,0.1,1.2,1,1.7,1.1,1.4,1.3,0.8,0.5,0.5,0.8,0.4,1.2,0.4,1.4,0.7,1.2,0.6,1.1,0.5,0.4,0.6,1.5,1.3,0,0.7,1.3,0.3,1.4,1,1.4,0.8,1.4,0.8,1.8,0.8,0.3,0,0.2,0.8,0.4,0,0.6,0.7,1.7,1,1.2,2,1.1,0.6,2,0.8,0.7,0.3,0.1,0.4,0.2,0.3,0.1,0.1,0.5,0.8,0.1,0.1,1.4,1.6,1.4,0.7,1.3,0.1,0.1,0,0,0,0.9,0.3,1.8,0,0,1,0,0.1,0.9,0.7,1.2,1.1,1.5,1.4,0.6,1.3,0.5,1.5,0.4,0.3,0.6,0.3,0.1,0.4,0.3,0.2,0.7,0.5,0.6,1.4,0.5,1.4,0.6,1.7,1.1,1.9,0.8,1.3,1.3,1.7,0.9,1.7,2.2,2,0.9,1.5,2.1,1,1.4,0.3,1.3,1.2,1.4,0.8,1.4,1.4,0.9,0.5,0.5,0.7,0.5,0.5,0.8,1.6,1,0.9,0.8,1.5,1.3,1.5,1.5,1.7,1.8,0.5,0.4,2,0,0.9,0.6,1,0,0,1,1.8,0,0.1,0.2,0.1,0.2,0,0.1,0.4,0.4,0.3,0.4,0.6,0.4,0.6,0.5]}]},{%22type%22:%22ProteinView%22,%22uniprotId%22:%22P04637%22,%22transcriptId%22:%22NM_000546.6%22,%22connectedViewId%22:%22lgv1%22,%22connectedView%22:{%22assembly%22:%22hg38%22,%22loc%22:%22chr17:7,661,779-7,687,538%22,%22tracks%22:[%22hg38-ncbiRefSeqSelect%22]}}]}):
20
+ TP53 in hg38, a vertebrate p53 alignment carrying ClinVar, AlphaMissense and
21
+ MaveDB per-residue tracks, and the AlphaFold model of P04637. Hovering a codon,
22
+ a column or a residue lights the other two views.
23
+ [Launching](docs/launching.md#the-readme-demo-taken-apart) takes the link apart.
24
+
25
+ An older shared session:
17
26
  https://jbrowse.org/code/jb2/main/index.html?config=https://unpkg.com/jbrowse-plugin-msaview/dist/config.json&session=share-BVmmEYAoAv&password=SuQaN
18
27
 
19
28
  ## Features
@@ -35,7 +44,7 @@ https://jbrowse.org/code/jb2/main/index.html?config=https://unpkg.com/jbrowse-pl
35
44
  - Aligns and builds trees in the browser, so a UniRef launch needs no job at any
36
45
  external service; EBI's aligners remain an option
37
46
  - Every launch is also a session-spec URL (`orthologParams`, `searchParams`,
38
- `connectedTranscript`), see [DEVELOPERS.md](DEVELOPERS.md)
47
+ `connectedTranscript`), see [launching](docs/launching.md)
39
48
 
40
49
  ## File format supports
41
50
 
@@ -46,30 +55,43 @@ https://jbrowse.org/code/jb2/main/index.html?config=https://unpkg.com/jbrowse-pl
46
55
  - Clustal files (e.g. .aln file, uses clustal-js parser)
47
56
  - Newick (tree can be loaded separately as a .nh file)
48
57
 
49
- ## Publication
58
+ ## Availability
50
59
 
51
- If you find this tool useful please cite our work
60
+ This plugin is installed by default on https://genomes.jbrowse.org so you can
61
+ use it on any species there
52
62
 
53
- Diesh, C., Stevens, G., Bridge, C., Hogue, G., Buels, R., Cain, S., Stein, L., &
54
- Holmes, I. (2026). Proteins in the Genome Browser: Integration of Phylogenies,
55
- Alignments, and Structures With Nucleotide-level Evidence in JBrowse 2. Journal
56
- of Molecular Biology, 169645. https://doi.org/10.1016/j.jmb.2026.169645
63
+ ## Documentation
57
64
 
58
- See also https://github.com/GMOD/proteinbrowser for overview
65
+ Using the plugin:
59
66
 
60
- ## Availability
67
+ - [Alignments from a gene](docs/alignments-from-a-gene.md): the launch dialog's
68
+ sources — NCBI, PANTHER and UniRef orthologs, EBI searches — and aligning in
69
+ the browser.
70
+ - [Your own alignments](docs/your-own-alignments.md): opening an alignment you
71
+ made, from a file, a link, or a per-gene dataset a site configures.
72
+ - [BLAST](docs/blast.md): why searches run at EBI rather than NCBI, and the
73
+ manual route to NCBI's `nr`.
61
74
 
62
- This plugin is installed by default on https://genomes.jbrowse.org so you can
63
- use it on any species there
75
+ Linking and embedding:
64
76
 
65
- ## Programmatic usage
77
+ - [Launching from a URL or code](docs/launching.md): worked session-spec links,
78
+ the genome connection, highlights, opening on a residue, and placement.
79
+ - [Launch parameters](docs/launch-parameters.md): every argument and field.
80
+ - [Linked views](docs/linked-views.md): how a column reaches its codon and a
81
+ protein3d structure.
66
82
 
67
- See [DEVELOPERS.md](DEVELOPERS.md)
83
+ Working on the plugin:
84
+
85
+ - [DEVELOPERS.md](DEVELOPERS.md): running it locally, the checks, the README
86
+ figure and publishing.
87
+
88
+ ## Publication
68
89
 
69
- ## Publishing
90
+ If you find this tool useful please cite our work
70
91
 
71
- just push a new tag using e.g.
92
+ Diesh, C., Stevens, G., Bridge, C., Hogue, G., Buels, R., Cain, S., Stein, L., &
93
+ Holmes, I. (2026). Proteins in the Genome Browser: Integration of Phylogenies,
94
+ Alignments, and Structures With Nucleotide-level Evidence in JBrowse 2. Journal
95
+ of Molecular Biology, 169645. https://doi.org/10.1016/j.jmb.2026.169645
72
96
 
73
- ```
74
- pnpm version minor
75
- ```
97
+ See also https://github.com/GMOD/proteinbrowser for overview
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { SimpleFeature } from '@jbrowse/core/util';
2
+ import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { transcriptPosToVisibleCol } from '../MsaViewPanel/util';
5
+ import { getProteinSequenceFromFeature } from './components/calculateProteinSequence';
6
+ import { findQueryRow } from './detectQueryRow';
7
+ import { cleanProteinSequence } from './util';
8
+ describe('cleanProteinSequence', () => {
9
+ test('drops the stop and a trailing partial codon', () => {
10
+ expect(cleanProteinSequence('MAWK*')).toBe('MAWK');
11
+ expect(cleanProteinSequence('MAWK&')).toBe('MAWK');
12
+ });
13
+ test('keeps a residue for every other codon', () => {
14
+ expect(cleanProteinSequence('&MAWK*')).toBe('XMAWK');
15
+ expect(cleanProteinSequence('MA*WK*')).toBe('MAXWK');
16
+ expect(cleanProteinSequence('MA&WK')).toBe('MAXWK');
17
+ });
18
+ });
19
+ // what the genome view hovers is a codon, and what lights is the query row's
20
+ // residue at g2p's number for that codon, so the two have to count alike
21
+ describe('a launched query row read through g2p', () => {
22
+ function rowResidueUnder(genomePos, json) {
23
+ const seq = json.seq;
24
+ const feature = new SimpleFeature(json);
25
+ const row = cleanProteinSequence(getProteinSequenceFromFeature({ seq, feature }));
26
+ const { g2p } = genomeToTranscriptSeqMapping(feature.toJSON());
27
+ const col = transcriptPosToVisibleCol({
28
+ querySeqName: 'QUERY',
29
+ querySeqOffset: 0,
30
+ seqPosToVisibleCol: (_, p) => (p < row.length ? p : row.length),
31
+ visibleColToSeqPos: (_, c) => (c < row.length ? c : undefined),
32
+ }, g2p[genomePos]);
33
+ return row[col];
34
+ }
35
+ function transcript(seq, cds) {
36
+ return {
37
+ uniqueId: 't1',
38
+ refName: 'chr1',
39
+ start: 0,
40
+ end: seq.length,
41
+ strand: 1,
42
+ type: 'mRNA',
43
+ seq,
44
+ subfeatures: [
45
+ { uniqueId: 'c1', refName: 'chr1', start: 0, end: seq.length, ...cds },
46
+ ],
47
+ };
48
+ }
49
+ test('after a partial first codon', () => {
50
+ // G | ATG GCC TGG AAA TAA: base 4 opens GCC, alanine
51
+ const json = transcript('GATGGCCTGGAAATAA', { type: 'CDS', phase: 1 });
52
+ expect(rowResidueUnder(4, json)).toBe('A');
53
+ expect(rowResidueUnder(10, json)).toBe('K');
54
+ });
55
+ test('after an internal stop', () => {
56
+ // ATG TGA GCC TGG TAA: a selenocysteine GENCODE does not annotate
57
+ const json = transcript('ATGTGAGCCTGGTAA', { type: 'CDS' });
58
+ expect(rowResidueUnder(6, json)).toBe('A');
59
+ });
60
+ });
61
+ describe('query row detection', () => {
62
+ test('matches a row a search launch built from a partial transcript', () => {
63
+ const msa = '>QUERY\nXMAWK\n>other\nMAWR-';
64
+ expect(findQueryRow(msa, '&MAWK*').match).toMatchObject({
65
+ name: 'QUERY',
66
+ quality: 'exact',
67
+ offset: 0,
68
+ });
69
+ });
70
+ test('still places a row that starts at the first whole codon', () => {
71
+ const msa = '>Query_1\nMAWK\n>other\nMAWR';
72
+ expect(findQueryRow(msa, '&MAWK*').match).toMatchObject({
73
+ name: 'Query_1',
74
+ offset: 1,
75
+ });
76
+ });
77
+ });
@@ -9,10 +9,10 @@ import LaunchPanelContent from '../LaunchPanelContent';
9
9
  import SequenceStatusMessage from '../SequenceStatus';
10
10
  import SubmitCancelActions from '../SubmitCancelActions';
11
11
  import TranscriptSelector from '../TranscriptSelector';
12
+ import { builtAlignmentLook, launchConnectedView, useLaunchSubmit, } from '../launchConnectedView';
12
13
  import { useTranscriptSelection } from '../useTranscriptSelection';
13
14
  import CachedBlastResults from './CachedBlastResults';
14
15
  import MsaAlgorithmSelect from './MsaAlgorithmSelect';
15
- import { blastLaunchView } from './blastLaunchView';
16
16
  import { databaseLabel, databaseOptionsFor, defaultMaxHits, defaultSearchFor, searchProgramLabels, searchPrograms, } from './consts';
17
17
  import { useStoredMsaAlgorithm, useStoredSearchChoice, } from './searchChoiceStorage';
18
18
  import { useCachedBlastResults } from './useCachedBlastResults';
@@ -35,7 +35,7 @@ const useStyles = makeStyles()({
35
35
  const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, children, preferredTranscriptId, }) {
36
36
  const { classes } = useStyles();
37
37
  const view = getLinearGenomeView(model);
38
- const [launchViewError, setLaunchViewError] = useState();
38
+ const { launchError, submit } = useLaunchSubmit(handleClose);
39
39
  // one piece of state, not two: a program and a database that program does not
40
40
  // have is a 400 from EBI minutes after Submit, and holding them apart is what
41
41
  // would let them drift into that
@@ -53,7 +53,7 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
53
53
  preferredTranscriptId,
54
54
  });
55
55
  const { selectedTranscript, proteinSequence, sequenceStatus } = transcriptSelection;
56
- const e = transcriptSelection.error ?? launchViewError ?? cachedResultsError;
56
+ const e = transcriptSelection.error ?? launchError ?? cachedResultsError;
57
57
  return (React.createElement(React.Fragment, null,
58
58
  React.createElement(LaunchPanelContent, { error: e },
59
59
  children,
@@ -85,37 +85,34 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
85
85
  React.createElement(Typography, null, "Previous BLAST Results")),
86
86
  React.createElement(AccordionDetails, null,
87
87
  React.createElement(CachedBlastResults, { model: model, handleClose: handleClose, feature: feature })))) : null),
88
- React.createElement(SubmitCancelActions, { model: model, hint: React.createElement(SequenceStatusMessage, { status: sequenceStatus }), submitDisabled: !proteinSequence || !hitCountValid, onSubmit: () => {
89
- try {
90
- if (selectedTranscript) {
91
- setLaunchViewError(undefined);
92
- blastLaunchView({
93
- feature: selectedTranscript,
88
+ React.createElement(SubmitCancelActions, { model: model, hint: React.createElement(SequenceStatusMessage, { status: sequenceStatus }), submitDisabled: !proteinSequence || !hitCountValid, onSubmit: placement => {
89
+ if (selectedTranscript) {
90
+ const blastParams = search.program === 'phmmer'
91
+ ? {
92
+ searchProgram: 'phmmer',
93
+ blastDatabase: search.database,
94
+ maxHits: hitCount,
95
+ selectedTranscript: selectedTranscript.toJSON(),
96
+ proteinSequence,
97
+ }
98
+ : {
99
+ searchProgram: 'blastp',
100
+ blastDatabase: search.database,
101
+ msaAlgorithm: selectedMsaAlgorithm,
102
+ maxHits: hitCount,
103
+ selectedTranscript: selectedTranscript.toJSON(),
104
+ proteinSequence,
105
+ };
106
+ submit(() => {
107
+ launchConnectedView({
94
108
  view,
95
- newViewTitle: getBlastViewTitle(feature, selectedTranscript),
96
- blastParams: search.program === 'phmmer'
97
- ? {
98
- searchProgram: 'phmmer',
99
- blastDatabase: search.database,
100
- maxHits: hitCount,
101
- selectedTranscript: selectedTranscript.toJSON(),
102
- proteinSequence,
103
- }
104
- : {
105
- searchProgram: 'blastp',
106
- blastDatabase: search.database,
107
- msaAlgorithm: selectedMsaAlgorithm,
108
- maxHits: hitCount,
109
- selectedTranscript: selectedTranscript.toJSON(),
110
- proteinSequence,
111
- },
109
+ feature: selectedTranscript,
110
+ placement,
111
+ displayName: getBlastViewTitle(feature, selectedTranscript),
112
+ ...builtAlignmentLook,
113
+ blastParams,
112
114
  });
113
- handleClose();
114
- }
115
- }
116
- catch (e) {
117
- console.error(e);
118
- setLaunchViewError(e);
115
+ });
119
116
  }
120
117
  }, onCancel: handleClose })));
121
118
  });
@@ -8,10 +8,10 @@ import TextField2 from '../../../components/TextField2';
8
8
  import { useQueryRowName } from '../../useQueryRowName';
9
9
  import { cleanProteinSequence, getGeneDisplayName, getLinearGenomeView, } from '../../util';
10
10
  import LaunchPanelContent from '../LaunchPanelContent';
11
- import { launchView } from '../ManualMSALoader/launchView';
12
11
  import QueryRowSelector from '../QueryRowSelector';
13
12
  import SubmitCancelActions from '../SubmitCancelActions';
14
13
  import TranscriptSelector from '../TranscriptSelector';
14
+ import { launchConnectedView, useLaunchSubmit } from '../launchConnectedView';
15
15
  import { useTranscriptSelection } from '../useTranscriptSelection';
16
16
  import { BASE_BLAST_URL } from './consts';
17
17
  const useStyles = makeStyles()({
@@ -46,7 +46,7 @@ const useStyles = makeStyles()({
46
46
  const BlastManualPanel = observer(function ({ handleClose, feature, model, children, preferredTranscriptId, }) {
47
47
  const { classes } = useStyles();
48
48
  const view = getLinearGenomeView(model);
49
- const [launchViewError, setLaunchViewError] = useState();
49
+ const { launchError, submit } = useLaunchSubmit(handleClose);
50
50
  const [msaText, setMsaText] = useState('');
51
51
  const [treeText, setTreeText] = useState('');
52
52
  const transcriptSelection = useTranscriptSelection({
@@ -62,7 +62,7 @@ const BlastManualPanel = observer(function ({ handleClose, feature, model, child
62
62
  const link = `${BASE_BLAST_URL}?PAGE_TYPE=BlastSearch&PAGE=Proteins&PROGRAM=blastp&QUERY=${s2}`;
63
63
  const link2 = `${BASE_BLAST_URL}?PAGE_TYPE=BlastSearch&PAGE=Proteins&PROGRAM=blastp&QUERY=${shorten2(s2, 10)}`;
64
64
  return (React.createElement(React.Fragment, null,
65
- React.createElement(LaunchPanelContent, { error: launchViewError ?? error },
65
+ React.createElement(LaunchPanelContent, { error: launchError ?? error },
66
66
  children,
67
67
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
68
68
  React.createElement("div", { className: classes.step },
@@ -83,24 +83,19 @@ const BlastManualPanel = observer(function ({ handleClose, feature, model, child
83
83
  setTreeText(event.target.value);
84
84
  } }),
85
85
  React.createElement(QueryRowSelector, { ...queryRow })))),
86
- React.createElement(SubmitCancelActions, { model: model, submitDisabled: !selectedTranscript || !msaText.trim(), onSubmit: () => {
87
- try {
88
- if (selectedTranscript) {
89
- setLaunchViewError(undefined);
90
- launchView({
91
- newViewTitle: getGeneDisplayName(selectedTranscript),
86
+ React.createElement(SubmitCancelActions, { model: model, submitDisabled: !selectedTranscript || !msaText.trim(), onSubmit: placement => {
87
+ if (selectedTranscript) {
88
+ submit(() => {
89
+ launchConnectedView({
92
90
  view,
93
91
  feature: selectedTranscript,
92
+ placement,
93
+ displayName: getGeneDisplayName(selectedTranscript),
94
94
  querySeqName: queryRow.querySeqName,
95
95
  querySeqOffset: queryRow.querySeqOffset,
96
96
  data: { msa: msaText, tree: treeText },
97
97
  });
98
- handleClose();
99
- }
100
- }
101
- catch (e) {
102
- console.error(e);
103
- setLaunchViewError(e);
98
+ });
104
99
  }
105
100
  }, onCancel: handleClose })));
106
101
  });
@@ -5,7 +5,8 @@ import { Button, IconButton, List, ListItem, ListItemButton, ListItemText, Typog
5
5
  import { observer } from 'mobx-react';
6
6
  import { makeStyles } from 'tss-react/mui';
7
7
  import { featureMatchesId, getGeneIdentifiers, getLinearGenomeView, getSortedTranscriptFeatures, } from '../../util';
8
- import { blastLaunchViewFromCache } from './blastLaunchView';
8
+ import { builtAlignmentLook, launchConnectedView } from '../launchConnectedView';
9
+ import { useLaunchPlacement } from '../launchPlacement';
9
10
  import { useCachedBlastResults } from './useCachedBlastResults';
10
11
  const useStyles = makeStyles()({
11
12
  header: {
@@ -53,22 +54,22 @@ const CachedBlastResults = observer(function ({ model, handleClose, feature, })
53
54
  const { classes } = useStyles();
54
55
  const view = getLinearGenomeView(model);
55
56
  const [operationError, setOperationError] = useState();
57
+ const [sideBySide] = useLaunchPlacement();
56
58
  const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature]);
57
59
  const { results, error, isLoading, handleDelete, handleClearAll } = useCachedBlastResults(geneIds);
58
60
  const handleUseCached = (cached) => {
59
- // reconnect the cached MSA to the genome: the cached query row is named
60
- // 'QUERY' (react-msaview's default querySeqName) and corresponds to the
61
- // transcript stored as transcriptId. Resolving it here restores the
62
- // MSA<->genome navigation and hover-sync a fresh BLAST gets.
63
- const { transcriptId } = cached;
64
- const transcript = transcriptId
65
- ? getSortedTranscriptFeatures(feature).find(t => featureMatchesId(t, transcriptId))
66
- : undefined;
67
- blastLaunchViewFromCache({
61
+ // the cached query row is the plugin's default `QUERY`, translated from
62
+ // the transcript stored as transcriptId, so that transcript relinks it
63
+ const { transcriptId, msa, tree, treeMetadata } = cached;
64
+ launchConnectedView({
68
65
  view,
69
- cached,
70
- newViewTitle: `BLAST - ${getResultDisplayName(cached)}`,
71
- connectedFeature: transcript?.toJSON(),
66
+ feature: transcriptId
67
+ ? getSortedTranscriptFeatures(feature).find(t => featureMatchesId(t, transcriptId))
68
+ : undefined,
69
+ placement: sideBySide ? 'splitRight' : 'stack',
70
+ displayName: `BLAST - ${getResultDisplayName(cached)}`,
71
+ ...builtAlignmentLook,
72
+ data: { msa, tree, treeMetadata },
72
73
  });
73
74
  handleClose();
74
75
  };
@@ -1,9 +1,11 @@
1
1
  import React, { useState } from 'react';
2
2
  import { FileSelector } from '@jbrowse/core/ui';
3
+ import { openLocation } from '@jbrowse/core/util/io';
3
4
  import { FormControl, FormControlLabel, Radio, RadioGroup } from '@mui/material';
4
5
  import { observer } from 'mobx-react';
5
6
  import { makeStyles } from 'tss-react/mui';
6
7
  import TextField2 from '../../../components/TextField2';
8
+ import { useDebounced, useFetch } from '../../../utils/useFetch';
7
9
  import { useQueryRowName } from '../../useQueryRowName';
8
10
  import { getGeneDisplayName, getLinearGenomeView } from '../../util';
9
11
  import LaunchPanelContent from '../LaunchPanelContent';
@@ -11,8 +13,18 @@ import QueryRowSelector from '../QueryRowSelector';
11
13
  import SequenceStatusMessage from '../SequenceStatus';
12
14
  import SubmitCancelActions from '../SubmitCancelActions';
13
15
  import TranscriptSelector from '../TranscriptSelector';
16
+ import { launchConnectedView, useLaunchSubmit } from '../launchConnectedView';
14
17
  import { useTranscriptSelection } from '../useTranscriptSelection';
15
- import { launchView } from './launchView';
18
+ /**
19
+ * The chosen file's text, so its query row is found by sequence the way a
20
+ * pasted alignment's is; without it a file launch named no row and never
21
+ * linked to the genome. Debounced because a URL arrives a keystroke at a time.
22
+ */
23
+ function useMsaFileText(location) {
24
+ const debounced = useDebounced(location, 500);
25
+ const { data } = useFetch(debounced ? [JSON.stringify(debounced), 'msa-file-text'] : null, () => openLocation(debounced).readFile('utf8'));
26
+ return data ?? '';
27
+ }
16
28
  const useStyles = makeStyles()({
17
29
  textAreaFont: {
18
30
  fontFamily: 'Courier New',
@@ -30,7 +42,7 @@ const useStyles = makeStyles()({
30
42
  const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handleClose, preferredTranscriptId, }) {
31
43
  const view = getLinearGenomeView(model);
32
44
  const { classes } = useStyles();
33
- const [launchViewError, setLaunchViewError] = useState();
45
+ const { launchError, submit } = useLaunchSubmit(handleClose);
34
46
  const [inputMethod, setInputMethod] = useState('file');
35
47
  const [msaText, setMsaText] = useState('');
36
48
  const [treeText, setTreeText] = useState('');
@@ -42,8 +54,9 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
42
54
  preferredTranscriptId,
43
55
  });
44
56
  const { selectedTranscript, proteinSequence, error, sequenceStatus } = transcriptSelection;
45
- const queryRow = useQueryRowName(msaText, proteinSequence);
46
- const e = launchViewError ?? error;
57
+ const msaFileText = useMsaFileText(inputMethod === 'file' ? msaFileLocation : undefined);
58
+ const queryRow = useQueryRowName(inputMethod === 'file' ? msaFileText : msaText, proteinSequence);
59
+ const e = launchError ?? error;
47
60
  return (React.createElement(React.Fragment, null,
48
61
  React.createElement(LaunchPanelContent, { error: e },
49
62
  React.createElement(FormControl, { component: "fieldset" },
@@ -65,14 +78,14 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
65
78
  React.createElement(QueryRowSelector, { ...queryRow })),
66
79
  React.createElement(SubmitCancelActions, { model: model, hint: React.createElement(SequenceStatusMessage, { status: sequenceStatus }), submitDisabled: !selectedTranscript ||
67
80
  (inputMethod === 'file' && !msaFileLocation) ||
68
- (inputMethod === 'text' && !msaText.trim()), onSubmit: () => {
69
- try {
70
- if (selectedTranscript) {
71
- setLaunchViewError(undefined);
72
- launchView({
73
- newViewTitle: getGeneDisplayName(selectedTranscript),
81
+ (inputMethod === 'text' && !msaText.trim()), onSubmit: placement => {
82
+ if (selectedTranscript) {
83
+ submit(() => {
84
+ launchConnectedView({
74
85
  view,
75
86
  feature: selectedTranscript,
87
+ placement,
88
+ displayName: getGeneDisplayName(selectedTranscript),
76
89
  querySeqName: queryRow.querySeqName,
77
90
  querySeqOffset: queryRow.querySeqOffset,
78
91
  ...(inputMethod === 'file'
@@ -80,19 +93,9 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
80
93
  msaFilehandle: msaFileLocation,
81
94
  treeFilehandle: treeFileLocation,
82
95
  }
83
- : {
84
- data: {
85
- msa: msaText,
86
- tree: treeText,
87
- },
88
- }),
96
+ : { data: { msa: msaText, tree: treeText } }),
89
97
  });
90
- handleClose();
91
- }
92
- }
93
- catch (err) {
94
- console.error(err);
95
- setLaunchViewError(err);
98
+ });
96
99
  }
97
100
  }, onCancel: handleClose })));
98
101
  });
@@ -4,7 +4,6 @@ import { observer } from 'mobx-react';
4
4
  import { makeStyles } from 'tss-react/mui';
5
5
  import TextField2 from '../../../components/TextField2';
6
6
  import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs';
7
- import { useLocalStorage } from '../../../utils/useLocalStorage';
8
7
  import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
9
8
  import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect';
10
9
  import { useStoredMsaAlgorithm } from '../BlastQuery/searchChoiceStorage';
@@ -12,10 +11,10 @@ import LaunchPanelContent from '../LaunchPanelContent';
12
11
  import SequenceStatusMessage from '../SequenceStatus';
13
12
  import SubmitCancelActions from '../SubmitCancelActions';
14
13
  import TranscriptSelector from '../TranscriptSelector';
14
+ import { builtAlignmentLook, launchConnectedView, useLaunchSubmit, } from '../launchConnectedView';
15
15
  import { useTranscriptSelection } from '../useTranscriptSelection';
16
- import OrthologSourceSelect, { ORTHOLOG_SOURCE_STORAGE_KEY, } from './OrthologSourceSelect';
16
+ import OrthologSourceSelect, { useStoredOrthologSource, } from './OrthologSourceSelect';
17
17
  import QuerySpeciesSelect from './QuerySpeciesSelect';
18
- import { orthologLaunchView } from './orthologLaunchView';
19
18
  const useStyles = makeStyles()({
20
19
  selectField: {
21
20
  width: 180,
@@ -31,9 +30,9 @@ const rowsHint = {
31
30
  const OrthologPanel = observer(function ({ handleClose, feature, model, preferredTranscriptId, }) {
32
31
  const { classes } = useStyles();
33
32
  const view = getLinearGenomeView(model);
34
- const [launchViewError, setLaunchViewError] = useState();
33
+ const { launchError, submit } = useLaunchSubmit(handleClose);
35
34
  const [taxId, setTaxId] = useState(9606);
36
- const [source, setSource] = useLocalStorage(ORTHOLOG_SOURCE_STORAGE_KEY, 'ncbi');
35
+ const [source, setSource] = useStoredOrthologSource();
37
36
  const [msaAlgorithm, setMsaAlgorithm] = useStoredMsaAlgorithm();
38
37
  const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies));
39
38
  const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
@@ -43,7 +42,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, preferre
43
42
  preferredTranscriptId,
44
43
  });
45
44
  const { selectedTranscript, proteinSequence, sequenceStatus } = transcriptSelection;
46
- const e = transcriptSelection.error ?? launchViewError;
45
+ const e = transcriptSelection.error ?? launchError;
47
46
  const rowCount = Number(maxSpecies);
48
47
  const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2;
49
48
  return (React.createElement(React.Fragment, null,
@@ -59,14 +58,15 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, preferre
59
58
  ? rowsHint[source](rowCount)
60
59
  : 'a whole number, 2 or more' })),
61
60
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection })),
62
- React.createElement(SubmitCancelActions, { model: model, hint: React.createElement(SequenceStatusMessage, { status: sequenceStatus }), submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
63
- try {
64
- if (selectedTranscript) {
65
- setLaunchViewError(undefined);
66
- orthologLaunchView({
67
- feature: selectedTranscript,
61
+ React.createElement(SubmitCancelActions, { model: model, hint: React.createElement(SequenceStatusMessage, { status: sequenceStatus }), submitDisabled: !proteinSequence || !rowCountValid, onSubmit: placement => {
62
+ if (selectedTranscript) {
63
+ submit(() => {
64
+ launchConnectedView({
68
65
  view,
69
- newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
66
+ feature: selectedTranscript,
67
+ placement,
68
+ displayName: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
69
+ ...builtAlignmentLook,
70
70
  orthologParams: {
71
71
  taxId,
72
72
  source,
@@ -77,12 +77,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, preferre
77
77
  proteinSequence,
78
78
  },
79
79
  });
80
- handleClose();
81
- }
82
- }
83
- catch (e) {
84
- console.error(e);
85
- setLaunchViewError(e);
80
+ });
86
81
  }
87
82
  }, onCancel: handleClose })));
88
83
  });
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import type { OrthologSource } from '../../../MsaViewPanel/model';
3
- export declare const ORTHOLOG_SOURCE_STORAGE_KEY = "msaview-ortholog-source";
4
3
  export declare const orthologSourceLabels: Record<OrthologSource, string>;
4
+ export declare function validOrthologSource(stored: unknown): OrthologSource;
5
+ export declare function useStoredOrthologSource(): readonly ["ncbi" | "panther" | "uniref", (source: OrthologSource) => void];
5
6
  export default function OrthologSourceSelect({ value, onChange, className, }: {
6
7
  value: OrthologSource;
7
8
  onChange: (val: OrthologSource) => void;
@@ -1,12 +1,28 @@
1
1
  import React from 'react';
2
2
  import { MenuItem } from '@mui/material';
3
3
  import TextField2 from '../../../components/TextField2';
4
- export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
4
+ import { useLocalStorage } from '../../../utils/useLocalStorage';
5
+ const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
5
6
  export const orthologSourceLabels = {
6
7
  ncbi: 'NCBI orthologs',
7
8
  panther: 'PANTHER',
8
9
  uniref: 'UniRef cluster',
9
10
  };
11
+ const orthologSources = Object.keys(orthologSourceLabels);
12
+ // another plugin version on the same origin may have stored a source this one
13
+ // lacks, and the tab indexes its hints by it
14
+ export function validOrthologSource(stored) {
15
+ return orthologSources.find(s => s === stored) ?? 'ncbi';
16
+ }
17
+ export function useStoredOrthologSource() {
18
+ const [stored, setStored] = useLocalStorage(ORTHOLOG_SOURCE_STORAGE_KEY, 'ncbi');
19
+ return [
20
+ validOrthologSource(stored),
21
+ (source) => {
22
+ setStored(source);
23
+ },
24
+ ];
25
+ }
10
26
  // Which species a source can answer for, in the words a reader picking one
11
27
  // needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
12
28
  // from human to yeast and Arabidopsis, and a UniRef cluster is every UniProtKB
@@ -19,5 +35,5 @@ const hints = {
19
35
  export default function OrthologSourceSelect({ value, onChange, className, }) {
20
36
  return (React.createElement(TextField2, { variant: "outlined", label: "Source", className: className, select: true, value: value, helperText: hints[value], onChange: event => {
21
37
  onChange(event.target.value);
22
- } }, Object.keys(orthologSourceLabels).map(val => (React.createElement(MenuItem, { value: val, key: val }, orthologSourceLabels[val])))));
38
+ } }, orthologSources.map(val => (React.createElement(MenuItem, { value: val, key: val }, orthologSourceLabels[val])))));
23
39
  }
@@ -0,0 +1,13 @@
1
+ import { expect, test } from 'vitest';
2
+ import { validOrthologSource } from './OrthologSourceSelect';
3
+ test('a stored source this version knows comes back as it went in', () => {
4
+ expect(validOrthologSource('panther')).toBe('panther');
5
+ expect(validOrthologSource('uniref')).toBe('uniref');
6
+ });
7
+ // the Orthologs tab is the dialog's default, and it looked its helper text up
8
+ // by whatever was stored, so an unknown value threw on open
9
+ test('anything else falls back to NCBI', () => {
10
+ expect(validOrthologSource('ensembl')).toBe('ncbi');
11
+ expect(validOrthologSource(undefined)).toBe('ncbi');
12
+ expect(validOrthologSource({ source: 'panther' })).toBe('ncbi');
13
+ });