jbrowse-plugin-msaview 3.4.1 → 3.6.0

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 (222) hide show
  1. package/README.md +7 -0
  2. package/dist/AddHighlightModel/index.js +3 -3
  3. package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.d.ts +8 -2
  4. package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +3 -3
  5. package/dist/BgzipFastaMsaAdapter/configSchema.js +1 -1
  6. package/dist/LaunchMsaView/codingFeature.d.ts +14 -0
  7. package/dist/LaunchMsaView/codingFeature.js +38 -0
  8. package/dist/LaunchMsaView/codingFeature.test.js +75 -0
  9. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.d.ts +3 -1
  10. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -26
  11. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.d.ts +3 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +7 -2
  13. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.d.ts +7 -3
  14. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +8 -7
  15. package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.js +2 -2
  16. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
  17. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +26 -4
  18. package/dist/LaunchMsaView/components/BlastQuery/consts.js +68 -2
  19. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +1 -1
  20. package/dist/LaunchMsaView/components/HelpButton.d.ts +2 -0
  21. package/dist/LaunchMsaView/components/HelpButton.js +17 -0
  22. package/dist/LaunchMsaView/components/HelpDialog.d.ts +4 -0
  23. package/dist/LaunchMsaView/components/HelpDialog.js +18 -0
  24. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.d.ts +3 -1
  25. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +21 -16
  26. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.d.ts +3 -1
  27. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +10 -4
  28. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
  29. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
  30. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +3 -1
  31. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +21 -7
  32. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
  33. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.d.ts +3 -1
  34. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +33 -15
  35. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +3 -1
  36. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +2 -1
  37. package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
  38. package/dist/LaunchMsaView/components/SequenceStatus.d.ts +14 -0
  39. package/dist/LaunchMsaView/components/SequenceStatus.js +15 -0
  40. package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
  41. package/dist/LaunchMsaView/components/SubmitCancelActions.js +25 -13
  42. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +33 -3
  43. package/dist/LaunchMsaView/components/TabPanel.js +15 -3
  44. package/dist/LaunchMsaView/components/TabPanel.test.d.ts +1 -0
  45. package/dist/LaunchMsaView/components/TabPanel.test.js +36 -0
  46. package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +3 -1
  47. package/dist/LaunchMsaView/components/TranscriptSelector.js +4 -2
  48. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +4 -1
  49. package/dist/LaunchMsaView/components/calculateProteinSequence.js +20 -10
  50. package/dist/LaunchMsaView/components/fetchSeq.d.ts +4 -1
  51. package/dist/LaunchMsaView/components/fetchSeq.js +14 -4
  52. package/dist/LaunchMsaView/components/launchPlacement.d.ts +8 -0
  53. package/dist/LaunchMsaView/components/launchPlacement.js +25 -0
  54. package/dist/LaunchMsaView/components/useFeatureSequence.d.ts +2 -0
  55. package/dist/LaunchMsaView/components/useFeatureSequence.js +14 -11
  56. package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +10 -1
  57. package/dist/LaunchMsaView/components/useTranscriptSelection.js +21 -3
  58. package/dist/LaunchMsaView/components/useTranscriptSelection.test.d.ts +1 -0
  59. package/dist/LaunchMsaView/components/useTranscriptSelection.test.js +57 -0
  60. package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
  61. package/dist/LaunchMsaView/detectQueryRow.js +11 -4
  62. package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
  63. package/dist/LaunchMsaView/index.js +28 -9
  64. package/dist/LaunchMsaView/launchTarget.d.ts +36 -8
  65. package/dist/LaunchMsaView/launchTarget.js +23 -13
  66. package/dist/LaunchMsaView/launchTarget.test.js +80 -15
  67. package/dist/LaunchMsaView/useQueryRowName.d.ts +13 -0
  68. package/dist/LaunchMsaView/useQueryRowName.js +16 -1
  69. package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
  70. package/dist/LaunchMsaView/useQueryRowName.test.js +40 -0
  71. package/dist/LaunchMsaView/util.js +5 -2
  72. package/dist/LaunchMsaViewExtensionPoint/index.js +8 -6
  73. package/dist/LaunchMsaViewExtensionPoint/index.test.js +21 -0
  74. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +9 -6
  75. package/dist/MsaViewPanel/afterCreateAutoruns.js +72 -28
  76. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +4 -3
  77. package/dist/MsaViewPanel/components/LaunchProgress.js +22 -10
  78. package/dist/MsaViewPanel/components/MsaViewPanel.js +10 -2
  79. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +47 -2
  80. package/dist/MsaViewPanel/doLaunchBlast.d.ts +8 -0
  81. package/dist/MsaViewPanel/doLaunchBlast.js +78 -93
  82. package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
  83. package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
  84. package/dist/MsaViewPanel/doLaunchOrthologs.js +40 -5
  85. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +10 -0
  86. package/dist/MsaViewPanel/genomeToMSA.js +25 -7
  87. package/dist/MsaViewPanel/genomeToMSA.test.js +214 -19
  88. package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
  89. package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
  90. package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
  91. package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
  92. package/dist/MsaViewPanel/model.d.ts +265 -103
  93. package/dist/MsaViewPanel/model.js +174 -48
  94. package/dist/MsaViewPanel/model.test.d.ts +1 -0
  95. package/dist/MsaViewPanel/model.test.js +166 -0
  96. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
  97. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
  98. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
  99. package/dist/MsaViewPanel/msaDataStore.d.ts +2 -0
  100. package/dist/MsaViewPanel/msaDataStore.js +10 -0
  101. package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
  102. package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
  103. package/dist/MsaViewPanel/processInit.test.js +72 -0
  104. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
  105. package/dist/MsaViewPanel/resolveConnectedTranscript.js +128 -0
  106. package/dist/MsaViewPanel/runLaunch.d.ts +1 -0
  107. package/dist/MsaViewPanel/runLaunch.js +35 -0
  108. package/dist/MsaViewPanel/runLaunch.test.js +46 -0
  109. package/dist/MsaViewPanel/storedData.test.js +20 -0
  110. package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
  111. package/dist/MsaViewPanel/structureConnection.js +0 -19
  112. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +12 -5
  113. package/dist/MsaViewPanel/util.d.ts +39 -0
  114. package/dist/MsaViewPanel/util.js +44 -0
  115. package/dist/index.d.ts +11 -11
  116. package/dist/jbrowse-plugin-msaview.umd.production.min.js +56 -68
  117. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  118. package/dist/utils/browserAlign.d.ts +42 -0
  119. package/dist/utils/browserAlign.js +297 -0
  120. package/dist/utils/browserAlign.test.d.ts +1 -0
  121. package/dist/utils/browserAlign.test.js +85 -0
  122. package/dist/utils/ebiBlast.d.ts +10 -1
  123. package/dist/utils/ebiBlast.js +27 -1
  124. package/dist/utils/homologSearch.d.ts +31 -0
  125. package/dist/utils/homologSearch.js +6 -0
  126. package/dist/utils/msa.d.ts +3 -11
  127. package/dist/utils/msa.js +14 -29
  128. package/dist/utils/msaRows.d.ts +11 -8
  129. package/dist/utils/msaRows.js +14 -12
  130. package/dist/utils/phmmer.d.ts +11 -1
  131. package/dist/utils/phmmer.js +49 -12
  132. package/dist/utils/unirefHomologs.d.ts +94 -0
  133. package/dist/utils/unirefHomologs.js +193 -0
  134. package/dist/utils/unirefHomologs.test.d.ts +1 -0
  135. package/dist/utils/unirefHomologs.test.js +118 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/package.json +23 -22
  139. package/src/AddHighlightModel/index.tsx +1 -1
  140. package/src/BgzipFastaMsaAdapter/configSchema.ts +1 -1
  141. package/src/LaunchMsaView/codingFeature.test.ts +96 -0
  142. package/src/LaunchMsaView/codingFeature.ts +49 -0
  143. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +52 -26
  144. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +9 -1
  145. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +24 -11
  146. package/src/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.tsx +4 -4
  147. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
  148. package/src/LaunchMsaView/components/BlastQuery/consts.ts +77 -2
  149. package/src/LaunchMsaView/components/HelpButton.tsx +33 -0
  150. package/src/LaunchMsaView/components/HelpDialog.tsx +78 -0
  151. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +54 -34
  152. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +13 -2
  153. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
  154. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +28 -5
  155. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
  156. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +63 -13
  157. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +4 -0
  158. package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
  159. package/src/LaunchMsaView/components/SequenceStatus.tsx +29 -0
  160. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +62 -3
  161. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +37 -15
  162. package/src/LaunchMsaView/components/TabPanel.test.tsx +51 -0
  163. package/src/LaunchMsaView/components/TabPanel.tsx +16 -4
  164. package/src/LaunchMsaView/components/TranscriptSelector.tsx +6 -1
  165. package/src/LaunchMsaView/components/calculateProteinSequence.ts +29 -10
  166. package/src/LaunchMsaView/components/fetchSeq.ts +25 -5
  167. package/src/LaunchMsaView/components/launchPlacement.tsx +41 -0
  168. package/src/LaunchMsaView/components/useFeatureSequence.ts +18 -11
  169. package/src/LaunchMsaView/components/useTranscriptSelection.test.tsx +66 -0
  170. package/src/LaunchMsaView/components/useTranscriptSelection.ts +41 -2
  171. package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
  172. package/src/LaunchMsaView/detectQueryRow.ts +22 -4
  173. package/src/LaunchMsaView/index.ts +32 -13
  174. package/src/LaunchMsaView/launchTarget.test.ts +88 -15
  175. package/src/LaunchMsaView/launchTarget.ts +49 -14
  176. package/src/LaunchMsaView/useQueryRowName.test.ts +54 -0
  177. package/src/LaunchMsaView/useQueryRowName.ts +22 -1
  178. package/src/LaunchMsaView/util.ts +8 -4
  179. package/src/LaunchMsaViewExtensionPoint/index.test.ts +23 -0
  180. package/src/LaunchMsaViewExtensionPoint/index.ts +32 -6
  181. package/src/MsaViewPanel/afterCreateAutoruns.ts +85 -27
  182. package/src/MsaViewPanel/components/LaunchProgress.tsx +38 -7
  183. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +50 -2
  184. package/src/MsaViewPanel/components/MsaViewPanel.tsx +10 -2
  185. package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
  186. package/src/MsaViewPanel/doLaunchBlast.ts +92 -142
  187. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +13 -0
  188. package/src/MsaViewPanel/doLaunchOrthologs.ts +55 -5
  189. package/src/MsaViewPanel/genomeToMSA.test.ts +251 -31
  190. package/src/MsaViewPanel/genomeToMSA.ts +35 -7
  191. package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
  192. package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
  193. package/src/MsaViewPanel/model.test.ts +196 -0
  194. package/src/MsaViewPanel/model.ts +204 -55
  195. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
  196. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
  197. package/src/MsaViewPanel/msaDataStore.ts +10 -0
  198. package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
  199. package/src/MsaViewPanel/processInit.test.ts +84 -0
  200. package/src/MsaViewPanel/resolveConnectedTranscript.ts +160 -0
  201. package/src/MsaViewPanel/runLaunch.test.ts +54 -0
  202. package/src/MsaViewPanel/runLaunch.ts +39 -0
  203. package/src/MsaViewPanel/storedData.test.ts +26 -0
  204. package/src/MsaViewPanel/structureConnection.ts +0 -26
  205. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +14 -5
  206. package/src/MsaViewPanel/util.ts +75 -0
  207. package/src/utils/browserAlign.test.ts +102 -0
  208. package/src/utils/browserAlign.ts +352 -0
  209. package/src/utils/ebiBlast.ts +34 -0
  210. package/src/utils/homologSearch.ts +51 -0
  211. package/src/utils/msa.ts +19 -41
  212. package/src/utils/msaRows.ts +20 -16
  213. package/src/utils/phmmer.ts +57 -11
  214. package/src/utils/unirefHomologs.test.ts +149 -0
  215. package/src/utils/unirefHomologs.ts +323 -0
  216. package/src/version.ts +1 -1
  217. package/dist/LaunchMsaView/components/geneticCodes.d.ts +0 -15
  218. package/dist/LaunchMsaView/components/geneticCodes.js +0 -227
  219. package/dist/MsaViewPanel/structureConnection.test.js +0 -53
  220. package/src/LaunchMsaView/components/geneticCodes.ts +0 -298
  221. package/src/MsaViewPanel/structureConnection.test.ts +0 -62
  222. /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/codingFeature.test.d.ts} +0 -0
@@ -1,3 +1,4 @@
1
+ import { isGeneLikeType } from './codingFeature';
1
2
  import type { MenuItem } from '@jbrowse/core/ui';
2
3
  import type { Feature } from '@jbrowse/core/util';
3
4
  export interface ContextMenuInfo {
@@ -13,13 +14,40 @@ export interface DisplayModel {
13
14
  fetchFullFeature?: (featureId: string, displayedRegionIndex: number) => Promise<Feature | undefined>;
14
15
  contextMenuFeature?: Feature;
15
16
  }
16
- export declare function isGeneLikeType(type: unknown): boolean;
17
17
  /**
18
- * How to get the right-clicked feature, or nothing when there is no gene to
19
- * launch on. Both host shapes reduce to a thunk, so the menu item is built and
20
- * the dialog is opened by one code path — and the same gene test decides both.
21
- * The strict three-name set the legacy branch used to carry disagreed with the
22
- * loose one above, so a `lnc_RNA` offered the menu item on a 4.3 host and not
23
- * on a 3.7 one.
18
+ * What the menu item launches on. A legacy host hands over the whole feature,
19
+ * so whether it codes for anything is known while the menu is built; a canvas
20
+ * host's hit test carries a type and an id, so that question can only be
21
+ * answered after the fetch, and the caller answers it there.
24
22
  */
25
- export declare function launchTarget(self: DisplayModel): (() => Promise<Feature | undefined>) | undefined;
23
+ interface ClickedTranscript {
24
+ /**
25
+ * The isoform the click actually landed on, when the dialog opens on its
26
+ * gene. Climbing to the gene is what puts every isoform in the picker, and it
27
+ * threw away which one the user pointed at: the picker then opened on the
28
+ * longest transcript, and on a v4 host that is the only place the choice was
29
+ * ever stated.
30
+ */
31
+ preferredTranscriptId?: string;
32
+ }
33
+ export type MenuTarget = (ClickedTranscript & {
34
+ feature: Feature;
35
+ }) | (ClickedTranscript & {
36
+ fetchFeature: () => Promise<Feature | undefined>;
37
+ });
38
+ export { isGeneLikeType };
39
+ /**
40
+ * How to get the right-clicked feature, or nothing when there is nothing to
41
+ * launch on. Both host shapes reduce to one target, so the menu item is built
42
+ * and the dialog is opened by one code path — and the same gene test decides
43
+ * both. The strict three-name set the legacy branch used to carry disagreed
44
+ * with the loose one above, so a `lnc_RNA` offered the menu item on a 4.3 host
45
+ * and not on a 3.7 one.
46
+ *
47
+ * Gene-like is not enough on its own: an lncRNA has no protein to align, and
48
+ * accepting it opened a dialog whose Submit never left grey, with nothing
49
+ * saying why. Where the whole feature is in hand the CDS decides here -- but
50
+ * only where there are subfeatures to read it off, since a feature that came
51
+ * with none has not answered the question.
52
+ */
53
+ export declare function launchTarget(self: DisplayModel): MenuTarget | undefined;
@@ -1,3 +1,4 @@
1
+ import { geneLikeRoot, isGeneLikeType, isKnownNonCoding } from './codingFeature';
1
2
  // Read off the clicked item rather than off the display.
2
3
  //
3
4
  // LinearBasicDisplay used to publish an `isGeneLike` getter and this gated on
@@ -12,17 +13,20 @@
12
13
  // costs one comparison. Deliberately the same loose case-insensitive test the
13
14
  // host applies (`isGeneLikeType` in collapseIntronsMenu.ts): real GFFs carry
14
15
  // 'mRNA', 'lnc_RNA', 'protein_coding_gene', 'transcript'.
15
- export function isGeneLikeType(type) {
16
- const t = String(type ?? '').toLowerCase();
17
- return t.includes('gene') || t.includes('rna') || t.includes('transcript');
18
- }
16
+ export { isGeneLikeType };
19
17
  /**
20
- * How to get the right-clicked feature, or nothing when there is no gene to
21
- * launch on. Both host shapes reduce to a thunk, so the menu item is built and
22
- * the dialog is opened by one code path — and the same gene test decides both.
23
- * The strict three-name set the legacy branch used to carry disagreed with the
24
- * loose one above, so a `lnc_RNA` offered the menu item on a 4.3 host and not
25
- * on a 3.7 one.
18
+ * How to get the right-clicked feature, or nothing when there is nothing to
19
+ * launch on. Both host shapes reduce to one target, so the menu item is built
20
+ * and the dialog is opened by one code path — and the same gene test decides
21
+ * both. The strict three-name set the legacy branch used to carry disagreed
22
+ * with the loose one above, so a `lnc_RNA` offered the menu item on a 4.3 host
23
+ * and not on a 3.7 one.
24
+ *
25
+ * Gene-like is not enough on its own: an lncRNA has no protein to align, and
26
+ * accepting it opened a dialog whose Submit never left grey, with nothing
27
+ * saying why. Where the whole feature is in hand the CDS decides here -- but
28
+ * only where there are subfeatures to read it off, since a feature that came
29
+ * with none has not answered the question.
26
30
  */
27
31
  export function launchTarget(self) {
28
32
  const info = self.contextMenuInfo;
@@ -32,11 +36,17 @@ export function launchTarget(self) {
32
36
  // rejects the click can only answer with some other feature
33
37
  if (info && fetchFullFeature) {
34
38
  return isGeneLikeType(info.item.type)
35
- ? () => fetchFullFeature(info.item.featureId, info.displayedRegionIndex)
39
+ ? {
40
+ fetchFeature: () => fetchFullFeature(info.item.featureId, info.displayedRegionIndex),
41
+ }
36
42
  : undefined;
37
43
  }
38
44
  const legacy = self.contextMenuFeature;
39
- return legacy && isGeneLikeType(legacy.get('type'))
40
- ? () => Promise.resolve(legacy)
45
+ if (!legacy) {
46
+ return undefined;
47
+ }
48
+ const root = geneLikeRoot(legacy);
49
+ return isGeneLikeType(root.get('type')) && !isKnownNonCoding(root)
50
+ ? { feature: root, preferredTranscriptId: legacy.id() }
41
51
  : undefined;
42
52
  }
@@ -1,19 +1,36 @@
1
+ import { SimpleFeature } from '@jbrowse/core/util';
1
2
  import { describe, expect, test } from 'vitest';
2
3
  import { isGeneLikeType, launchTarget } from './launchTarget';
3
- function feature(type) {
4
- return {
5
- get: (key) => (key === 'type' ? type : undefined),
6
- };
4
+ function feature(type, subfeatures = ['CDS']) {
5
+ return new SimpleFeature({
6
+ uniqueId: type,
7
+ refName: 'chr1',
8
+ start: 0,
9
+ end: 100,
10
+ type,
11
+ subfeatures: subfeatures.map((t, i) => ({
12
+ uniqueId: `${type}-${i}`,
13
+ refName: 'chr1',
14
+ start: 0,
15
+ end: 100,
16
+ type: t,
17
+ })),
18
+ });
7
19
  }
8
20
  const modernHost = (type) => ({
9
21
  contextMenuItems: () => [],
10
22
  contextMenuInfo: { item: { featureId: 'f1', type }, displayedRegionIndex: 0 },
11
23
  fetchFullFeature: (featureId) => Promise.resolve(feature(`fetched:${featureId}`)),
12
24
  });
13
- const legacyHost = (type) => ({
25
+ const legacyHost = (feat) => ({
14
26
  contextMenuItems: () => [],
15
- contextMenuFeature: feature(type),
27
+ contextMenuFeature: feat,
16
28
  });
29
+ async function fetched(target) {
30
+ return target && 'fetchFeature' in target
31
+ ? await target.fetchFeature()
32
+ : undefined;
33
+ }
17
34
  describe('isGeneLikeType', () => {
18
35
  test.each(['gene', 'mRNA', 'transcript', 'lnc_RNA', 'protein_coding_gene'])('accepts %s', type => {
19
36
  expect(isGeneLikeType(type)).toBe(true);
@@ -25,8 +42,8 @@ describe('isGeneLikeType', () => {
25
42
  describe('launchTarget', () => {
26
43
  test('resolves the clicked feature through fetchFullFeature', async () => {
27
44
  const target = launchTarget(modernHost('mRNA'));
28
- expect(target).toBeDefined();
29
- expect((await target())?.get('type')).toBe('fetched:f1');
45
+ expect(await fetched(target)).toBeDefined();
46
+ expect((await fetched(target))?.get('type')).toBe('fetched:f1');
30
47
  });
31
48
  test('offers nothing for a non-gene click', () => {
32
49
  expect(launchTarget(modernHost('CDS'))).toBeUndefined();
@@ -37,18 +54,66 @@ describe('launchTarget', () => {
37
54
  });
38
55
  // v3.7.0 hosts have contextMenuFeature and nothing else; dropping this
39
56
  // fallback once took "Launch MSA view" off every host in the wild.
40
- test('falls back to a synchronous contextMenuFeature', async () => {
41
- const target = launchTarget(legacyHost('mRNA'));
42
- expect(target).toBeDefined();
43
- expect((await target())?.get('type')).toBe('mRNA');
57
+ test('falls back to a synchronous contextMenuFeature', () => {
58
+ const target = launchTarget(legacyHost(feature('mRNA')));
59
+ expect(target && 'feature' in target && target.feature.get('type')).toBe('mRNA');
44
60
  });
45
61
  test('applies the same gene test on both host shapes', () => {
46
- expect(launchTarget(legacyHost('lnc_RNA'))).toBeDefined();
47
- expect(launchTarget(legacyHost('exon'))).toBeUndefined();
62
+ expect(launchTarget(legacyHost(feature('lnc_RNA')))).toBeDefined();
63
+ expect(launchTarget(legacyHost(feature('exon')))).toBeUndefined();
64
+ });
65
+ // gene-like is not enough: an lncRNA translates to nothing, and offering the
66
+ // item opened a dialog whose Submit never left grey
67
+ test('declines a gene-like feature with no CDS below it', () => {
68
+ expect(launchTarget(legacyHost(feature('lnc_RNA', ['exon'])))).toBeUndefined();
69
+ });
70
+ // a legacy host can hand over a bare record whose children are a separate
71
+ // query, and declining it takes the item off an ordinary gene with no word
72
+ test('offers the item for a gene that arrived with no subfeatures', () => {
73
+ expect(launchTarget(legacyHost(feature('gene', [])))).toBeDefined();
74
+ });
75
+ // a click on an isoform should open the dialog on the gene, so every
76
+ // transcript is there to pick from
77
+ test('climbs to the gene from a clicked isoform', () => {
78
+ const gene = new SimpleFeature({
79
+ uniqueId: 'gene',
80
+ refName: 'chr1',
81
+ start: 0,
82
+ end: 100,
83
+ type: 'gene',
84
+ subfeatures: [
85
+ {
86
+ uniqueId: 'mrna',
87
+ refName: 'chr1',
88
+ start: 0,
89
+ end: 100,
90
+ type: 'mRNA',
91
+ subfeatures: [
92
+ {
93
+ uniqueId: 'cds',
94
+ refName: 'chr1',
95
+ start: 0,
96
+ end: 100,
97
+ type: 'CDS',
98
+ },
99
+ ],
100
+ },
101
+ ],
102
+ });
103
+ const mrna = gene.get('subfeatures')[0];
104
+ const target = launchTarget(legacyHost(mrna));
105
+ expect(target && 'feature' in target && target.feature).toBe(gene);
106
+ // the gene is what the dialog opens on, but the picker still opens on the
107
+ // isoform the click landed on -- on a v4 host that click is the only place
108
+ // the choice is ever stated
109
+ expect(target?.preferredTranscriptId).toBe(mrna.id());
48
110
  });
49
111
  // a host that has both shapes must not fall through to the legacy branch and
50
112
  // launch on a stale feature when the click was not on a gene
51
113
  test('a non-gene click on a host carrying both shapes offers nothing', () => {
52
- expect(launchTarget({ ...modernHost('CDS'), ...legacyHost('mRNA') })).toBeUndefined();
114
+ expect(launchTarget({
115
+ ...modernHost('CDS'),
116
+ ...legacyHost(feature('mRNA')),
117
+ })).toBeUndefined();
53
118
  });
54
119
  });
@@ -6,10 +6,23 @@
6
6
  * effect writing back into state, and an override survives later edits to the
7
7
  * alignment because it is the one thing actually stored.
8
8
  */
9
+ /**
10
+ * The row to launch with. Detection by residues is the answer wherever it has
11
+ * one -- aligners rename the query on the way through, so a name proves
12
+ * nothing -- but a caller with a naming convention of its own can offer it as a
13
+ * fallback, and it is taken only when the alignment really carries that row.
14
+ * An empty result means there is no query row, which a launch has to refuse:
15
+ * the view opens, renders, and then never navigates.
16
+ */
17
+ export declare function resolveQueryRowName(queryRow: {
18
+ querySeqName: string;
19
+ names: string[];
20
+ }, fallback: string): string;
9
21
  export declare function useQueryRowName(msaText: string, proteinSequence: string): {
10
22
  detected: import("./detectQueryRow").QueryRowMatch | undefined;
11
23
  names: string[];
12
24
  querySeqName: string;
25
+ querySeqOffset: number;
13
26
  setQuerySeqName: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
14
27
  isAutoDetected: boolean;
15
28
  };
@@ -8,15 +8,30 @@ import { findQueryRow } from './detectQueryRow';
8
8
  * effect writing back into state, and an override survives later edits to the
9
9
  * alignment because it is the one thing actually stored.
10
10
  */
11
+ /**
12
+ * The row to launch with. Detection by residues is the answer wherever it has
13
+ * one -- aligners rename the query on the way through, so a name proves
14
+ * nothing -- but a caller with a naming convention of its own can offer it as a
15
+ * fallback, and it is taken only when the alignment really carries that row.
16
+ * An empty result means there is no query row, which a launch has to refuse:
17
+ * the view opens, renders, and then never navigates.
18
+ */
19
+ export function resolveQueryRowName(queryRow, fallback) {
20
+ return (queryRow.querySeqName || (queryRow.names.includes(fallback) ? fallback : ''));
21
+ }
11
22
  export function useQueryRowName(msaText, proteinSequence) {
12
23
  const [override, setOverride] = useState();
13
24
  // parsing runs on every keystroke in the paste box otherwise, and an
14
25
  // alignment of a few hundred rows is not free
15
26
  const { names, match } = useMemo(() => findQueryRow(msaText, proteinSequence), [msaText, proteinSequence]);
27
+ const querySeqName = override ?? match?.name ?? '';
16
28
  return {
17
29
  detected: match,
18
30
  names,
19
- querySeqName: override ?? match?.name ?? '',
31
+ querySeqName,
32
+ // only the detected row's offset is known; a row the user picked instead is
33
+ // taken at face value, residue 1 to residue 1
34
+ querySeqOffset: querySeqName === match?.name ? match.offset : 0,
20
35
  setQuerySeqName: setOverride,
21
36
  isAutoDetected: override === undefined && !!match,
22
37
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ // @vitest-environment jsdom
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { resolveQueryRowName, useQueryRowName } from './useQueryRowName';
5
+ const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD';
6
+ const trimmed = `>aligned_query\n${protein.slice(5, 45)}\n>other\nWRONGWRONGWRONGWRONGWRONGWRONGWRONGWRONG\n`;
7
+ describe('useQueryRowName', () => {
8
+ test('carries the detected row and where it starts', () => {
9
+ const { result } = renderHook(() => useQueryRowName(trimmed, protein));
10
+ expect(result.current.querySeqName).toBe('aligned_query');
11
+ expect(result.current.querySeqOffset).toBe(5);
12
+ });
13
+ // the offset was measured against the detected row; another row is a row the
14
+ // user vouched for, and nothing here knows where in the protein it sits
15
+ test('a row the user picks instead is taken at face value', () => {
16
+ const { result } = renderHook(() => useQueryRowName(trimmed, protein));
17
+ act(() => {
18
+ result.current.setQuerySeqName('other');
19
+ });
20
+ expect(result.current.querySeqName).toBe('other');
21
+ expect(result.current.querySeqOffset).toBe(0);
22
+ });
23
+ });
24
+ // a pre-loaded dataset names its rows by a convention of its own, and the
25
+ // residues can be too diverged for detection to find the row that is right
26
+ // there
27
+ describe('resolveQueryRowName', () => {
28
+ const names = ['ENST1_hg38', 'mm39', 'rn7'];
29
+ test('prefers the detected or picked row over any fallback', () => {
30
+ expect(resolveQueryRowName({ querySeqName: 'mm39', names }, 'ENST1_hg38')).toBe('mm39');
31
+ });
32
+ test('falls back to a name the alignment actually carries', () => {
33
+ expect(resolveQueryRowName({ querySeqName: '', names }, 'ENST1_hg38')).toBe('ENST1_hg38');
34
+ });
35
+ // the whole bug: the old panel launched with this name whether or not the
36
+ // file had the row, and a name no row carries navigates nowhere
37
+ test('answers nothing for a fallback the alignment does not carry', () => {
38
+ expect(resolveQueryRowName({ querySeqName: '', names }, 'ENST9_hg38')).toBe('');
39
+ });
40
+ });
@@ -2,11 +2,14 @@ import { getContainingView, sum } from '@jbrowse/core/util';
2
2
  export function getLinearGenomeView(model) {
3
3
  return getContainingView(model);
4
4
  }
5
+ function isNonEmptyString(v) {
6
+ return typeof v === 'string' && v !== '';
7
+ }
5
8
  function uniqueDefined(vals) {
6
- return [...new Set(vals.filter((v) => !!v))];
9
+ return [...new Set(vals.filter(isNonEmptyString))];
7
10
  }
8
11
  function joinDefined(sep, parts) {
9
- return parts.filter((p) => !!p).join(sep);
12
+ return parts.filter(isNonEmptyString).join(sep);
10
13
  }
11
14
  export function getTranscriptFeatures(feature) {
12
15
  // check if we are looking at a 'two-level' or 'three-level' feature by
@@ -1,15 +1,16 @@
1
1
  import { launchMsaView } from '../utils/launchMsaView';
2
2
  export default function LaunchMsaViewExtensionPointF(pluginManager) {
3
3
  pluginManager.addToExtensionPoint('LaunchView-MsaView', (args) => {
4
- const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, ...rest } = args;
5
- // `orthologParams` is a fourth source, and unlike the other three it names
6
- // no alignment at all — the view builds one from NCBI at launch, which is
7
- // the dialog's Orthologs tab reached declaratively.
4
+ const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, searchParams, ...rest } = args;
5
+ // `orthologParams` and `searchParams` name no alignment at all — the
6
+ // view builds one at launch, which is the dialog's Orthologs and BLAST
7
+ // tabs reached declaratively.
8
8
  if (!data &&
9
9
  !msaFileLocation &&
10
10
  !msaIndexedLocation &&
11
- !rest.orthologParams) {
12
- throw new Error('No MSA data, file location or orthologParams provided when launching MSA view');
11
+ !rest.orthologParams &&
12
+ !searchParams) {
13
+ throw new Error('No MSA data, file location, orthologParams or searchParams provided when launching MSA view');
13
14
  }
14
15
  // inline data and the tree URL are native react-msaview snapshot props, set
15
16
  // directly, and so is orthologParams (the model's own autorun picks it up).
@@ -27,6 +28,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
27
28
  };
28
29
  launchMsaView(session, {
29
30
  ...rest,
31
+ ...(searchParams ? { blastParams: searchParams } : {}),
30
32
  data,
31
33
  ...(treeFileLocation
32
34
  ? {
@@ -32,6 +32,27 @@ test('a file location travels through init', () => {
32
32
  querySeqName: undefined,
33
33
  });
34
34
  });
35
+ test('searchParams is a source, stored under the name the dialog uses', () => {
36
+ const snapshot = launch({
37
+ searchParams: {
38
+ searchProgram: 'phmmer',
39
+ blastDatabase: 'rp15',
40
+ accession: 'P04637',
41
+ },
42
+ connectedViewId: 'lgv1',
43
+ connectedTranscript: 'NM_000546.6',
44
+ });
45
+ expect(snapshot.blastParams).toEqual({
46
+ searchProgram: 'phmmer',
47
+ blastDatabase: 'rp15',
48
+ accession: 'P04637',
49
+ });
50
+ expect('searchParams' in snapshot).toBe(false);
51
+ expect(snapshot.connectedTranscript).toBe('NM_000546.6');
52
+ });
53
+ test('a launch naming no source at all is refused', () => {
54
+ expect(() => launch({ connectedViewId: 'lgv1' })).toThrow(/searchParams/);
55
+ });
35
56
  test('one field set is enough to need init', () => {
36
57
  const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
37
58
  expect(snapshot.init).toEqual({
@@ -12,16 +12,19 @@ export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
12
12
  * it is recorded whether or not the write succeeded, so a browser refusing
13
13
  * IndexedDB (private mode) fails once rather than in a loop.
14
14
  *
15
- * A view whose data comes from a filehandle stores nothing at all: the file is
16
- * the source of truth and react-msaview refetches it at startup.
15
+ * A view whose data comes from a filehandle -- or from the indexed block its
16
+ * kept `init` names -- stores nothing at all: the file is the source of truth
17
+ * and it is refetched at startup.
17
18
  */
18
19
  export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
19
20
  /**
20
21
  * Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
21
- * request, and clearing them on success is what marks it done. They are left in
22
- * place on failure so the error stays attributable to a specific request; the
23
- * autorun's only tracked read is orthologParams itself, so nothing refires
24
- * until a new request replaces them.
22
+ * request, and `launchCompleted` is what marks it done. They are left in place
23
+ * either way -- on failure so the error stays attributable to a specific
24
+ * request, and on success because they are the only durable statement of what
25
+ * the view is, which is what a stored alignment that expired is rebuilt from.
26
+ * The autorun tracks those two reads alone, so nothing refires until a new
27
+ * request replaces them or a retry clears the mark.
25
28
  */
26
29
  export declare function launchOrthologsIfNeeded(self: JBrowsePluginMsaViewModel): void;
27
30
  export declare function launchBlastIfNeeded(self: JBrowsePluginMsaViewModel): void;
@@ -8,8 +8,10 @@ import { loadProteinDomains } from './loadProteinDomains';
8
8
  import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
9
9
  import { runLaunch } from './runLaunch';
10
10
  import { getProteinViews } from './structureConnection';
11
- import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
12
- const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared. Relaunch the alignment to rebuild it.";
11
+ import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
12
+ const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared.";
13
+ const RELAUNCHABLE = ' Retry runs the original search again and rebuilds it.';
14
+ const START_OVER = ' Relaunch it from the gene to rebuild it.';
13
15
  export function loadStoredData(self) {
14
16
  const { dataStoreId, rows } = self;
15
17
  if (dataStoreId && rows.length === 0) {
@@ -35,10 +37,16 @@ export function loadStoredData(self) {
35
37
  }
36
38
  else {
37
39
  // the id names nothing, so clearing it is what lets react-msaview's
38
- // "Return to import form" actually return instead of landing back here
40
+ // "Return to import form" actually return instead of landing back
41
+ // here. The request that built the alignment is kept, though: it is
42
+ // the whole of a retry, and dropping it left the user reading an
43
+ // apology with nothing to press.
39
44
  transaction(() => {
40
45
  self.setDataStoreId(undefined);
41
- self.setError(new Error(EXPIRED_MESSAGE));
46
+ self.setError(new Error(EXPIRED_MESSAGE +
47
+ ((self.blastParams ?? self.orthologParams)
48
+ ? RELAUNCHABLE
49
+ : START_OVER)));
42
50
  });
43
51
  }
44
52
  }
@@ -73,16 +81,20 @@ function sameData(a, b) {
73
81
  * it is recorded whether or not the write succeeded, so a browser refusing
74
82
  * IndexedDB (private mode) fails once rather than in a loop.
75
83
  *
76
- * A view whose data comes from a filehandle stores nothing at all: the file is
77
- * the source of truth and react-msaview refetches it at startup.
84
+ * A view whose data comes from a filehandle -- or from the indexed block its
85
+ * kept `init` names -- stores nothing at all: the file is the source of truth
86
+ * and it is refetched at startup.
78
87
  */
79
88
  export function storeDataToIndexedDB(self) {
80
- const { rows, dataStoreId, isStoringData, lastStoredData } = self;
89
+ const { rows, dataStoreId, isStoringData, lastStoredData, init } = self;
81
90
  const data = currentData(self);
82
91
  if (rows.length === 0 ||
83
92
  isStoringData ||
84
93
  self.msaFilehandle ||
85
94
  self.treeFilehandle ||
95
+ // an indexed view keeps its init and refetches the block, so a row here
96
+ // would be one nothing ever reads
97
+ !!init?.msaIndexedLocation ||
86
98
  !(data.msa || data.tree) ||
87
99
  sameData(lastStoredData, data)) {
88
100
  return;
@@ -111,31 +123,47 @@ export function storeDataToIndexedDB(self) {
111
123
  }
112
124
  /**
113
125
  * Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
114
- * request, and clearing them on success is what marks it done. They are left in
115
- * place on failure so the error stays attributable to a specific request; the
116
- * autorun's only tracked read is orthologParams itself, so nothing refires
117
- * until a new request replaces them.
126
+ * request, and `launchCompleted` is what marks it done. They are left in place
127
+ * either way -- on failure so the error stays attributable to a specific
128
+ * request, and on success because they are the only durable statement of what
129
+ * the view is, which is what a stored alignment that expired is rebuilt from.
130
+ * The autorun tracks those two reads alone, so nothing refires until a new
131
+ * request replaces them or a retry clears the mark.
118
132
  */
119
133
  export function launchOrthologsIfNeeded(self) {
120
- if (self.orthologParams) {
134
+ if (self.orthologParams &&
135
+ !self.launchCompleted &&
136
+ !awaitingTranscript(self)) {
121
137
  runLaunch({
122
138
  self,
123
139
  message: 'Resolving orthologs',
124
140
  launch: scope => doLaunchOrthologs({ self, scope }),
141
+ // marked rather than dropped: the request is the only durable statement
142
+ // of what this view is, and a view whose stored alignment expired runs it
143
+ // again
125
144
  onLaunched: () => {
126
- self.setOrthologParams(undefined);
145
+ self.setLaunchCompleted(true);
127
146
  },
128
147
  });
129
148
  }
130
149
  }
150
+ /**
151
+ * A launch that names its transcript rather than its feature has to wait for
152
+ * the lookup: the query row is that transcript's translation, and the launch
153
+ * cannot start without a query. Both launchers read the same two fields, so
154
+ * the resolver setting `connectedFeature` is what refires them.
155
+ */
156
+ function awaitingTranscript(self) {
157
+ return !!self.connectedTranscript && !self.connectedFeature;
158
+ }
131
159
  export function launchBlastIfNeeded(self) {
132
- if (self.blastParams) {
160
+ if (self.blastParams && !self.launchCompleted && !awaitingTranscript(self)) {
133
161
  runLaunch({
134
162
  self,
135
163
  message: 'Submitting query',
136
164
  launch: scope => doLaunchBlast({ self, scope }),
137
165
  onLaunched: () => {
138
- self.setBlastParams(undefined);
166
+ self.setLaunchCompleted(true);
139
167
  },
140
168
  });
141
169
  }
@@ -167,15 +195,28 @@ export function autoLoadProteinDomains(self) {
167
195
  })();
168
196
  }
169
197
  }
170
- // Resolve the declarative `init` launch contract once, then clear it. msaUrl is
171
- // handed to react-msaview's native filehandle loader (openLocation + progress +
172
- // abort + CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip
173
- // name-indexed block is the one source with no native loader, so it's fetched
174
- // here. Inline data and tree URLs arrive as native snapshot props, not via init.
198
+ // Resolve the declarative `init` launch contract. msaUrl is handed to
199
+ // react-msaview's native filehandle loader (openLocation + progress + abort +
200
+ // CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip name-indexed
201
+ // block is the one source with no native loader, so it's fetched here. Inline
202
+ // data and tree URLs arrive as native snapshot props, not via init.
203
+ //
204
+ // An init that named the indexed block is KEPT rather than cleared, as
205
+ // jbrowse-plugin-tview keeps its own: what it resolves to is one alignment
206
+ // string, react-msaview drops a document over 50kb from the snapshot, and there
207
+ // is no filehandle to reload it from -- so a shared session came back saying the
208
+ // alignment had expired. The init is both smaller than what it fetches and the
209
+ // only durable statement of what the view is.
175
210
  export function processInit(self) {
176
211
  const { init } = self;
177
212
  if (init) {
178
213
  const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init;
214
+ const indexed = !!(msaIndexedLocation && msaName);
215
+ // a kept init re-runs this on every session restore; the alignment already
216
+ // in hand is the one it would fetch
217
+ if (indexed && self.data.msa) {
218
+ return;
219
+ }
179
220
  void (async () => {
180
221
  try {
181
222
  self.setError(undefined);
@@ -204,7 +245,9 @@ export function processInit(self) {
204
245
  throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
205
246
  }
206
247
  }
207
- self.setInit(undefined);
248
+ if (!indexed) {
249
+ self.setInit(undefined);
250
+ }
208
251
  }
209
252
  catch (e) {
210
253
  self.setError(e);
@@ -238,10 +281,10 @@ export function syncGenomeHoverToMsaColumn(self) {
238
281
  * Translate genome regions published by a 3D protein view into this MSA's
239
282
  * visible columns. The genome is the only coordinate space the two plugins
240
283
  * share, so the hops are genome coord -> protein position (the transcript's g2p
241
- * map) -> global alignment column -> visible column.
284
+ * map) -> visible column.
242
285
  */
243
286
  function genomeHighlightsToVisibleColumns(self, field) {
244
- const { connectedViewId, transcriptToMsaMap, querySeqName } = self;
287
+ const { connectedViewId, transcriptToMsaMap } = self;
245
288
  if (!transcriptToMsaMap || !hasQueryRow(self)) {
246
289
  return [];
247
290
  }
@@ -255,16 +298,17 @@ function genomeHighlightsToVisibleColumns(self, field) {
255
298
  for (const highlight of structure[field] ?? []) {
256
299
  for (let coord = highlight.start; coord < highlight.end; coord++) {
257
300
  const proteinPos = g2p[coord];
258
- if (proteinPos !== undefined) {
259
- columns.add(self.seqPosToGlobalCol(querySeqName, proteinPos));
301
+ const col = proteinPos === undefined
302
+ ? undefined
303
+ : transcriptPosToVisibleCol(self, proteinPos);
304
+ if (col !== undefined) {
305
+ columns.add(col);
260
306
  }
261
307
  }
262
308
  }
263
309
  }
264
310
  }
265
- return [...columns]
266
- .map(col => self.globalColToVisibleCol(col))
267
- .filter((col) => col !== undefined);
311
+ return [...columns];
268
312
  }
269
313
  function sameColumns(a, b) {
270
314
  if (!a || !b) {