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
@@ -4,6 +4,7 @@ import { cleanup, render, screen } from '@testing-library/react';
4
4
  import { afterEach, beforeEach, expect, test, vi } from 'vitest';
5
5
  import { LAUNCH_PLACEMENT_KEY } from '../../utils/workspaces';
6
6
  import SubmitCancelActions from './SubmitCancelActions';
7
+ import { LaunchPlacementProvider } from './launchPlacement';
7
8
  // getSession walks the MST tree, and this component only wants the two actions
8
9
  // off the far end of that walk
9
10
  vi.mock('@jbrowse/core/util', () => ({
@@ -39,14 +40,43 @@ test('a panel that launches nothing passes no model, and gets no box', () => {
39
40
  render(React.createElement(SubmitCancelActions, { onSubmit: () => { }, onCancel: () => { } }));
40
41
  expect(toggle()).toBeNull();
41
42
  });
42
- test('clicking it writes the placement the next launch will read', () => {
43
+ test('submitting writes the placement the launch will read', () => {
43
44
  render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { } }));
44
45
  toggle().click();
45
- expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('stack');
46
46
  expect(toggle().checked).toBe(false);
47
+ screen.getByText('Submit').click();
48
+ expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('stack');
49
+ });
50
+ // the box is a property of this launch until it is launched; a dialog the user
51
+ // backed out of should not have moved where every future one lands
52
+ test('cancelling leaves the stored placement alone', () => {
53
+ render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { } }));
47
54
  toggle().click();
55
+ screen.getByText('Cancel').click();
56
+ expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBeNull();
57
+ });
58
+ // a host with no tiling never shows the box, so submitting there must not
59
+ // overwrite the choice the user made on a host that does
60
+ test('a host that cannot tile writes nothing on submit', () => {
61
+ localStorage.setItem(LAUNCH_PLACEMENT_KEY, 'splitRight');
62
+ render(React.createElement(SubmitCancelActions, { model: trackModel({}), onSubmit: () => { }, onCancel: () => { } }));
63
+ screen.getByText('Submit').click();
48
64
  expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('splitRight');
49
- expect(toggle().checked).toBe(true);
65
+ });
66
+ // every visited tab stays mounted, so each has an actions row of its own; with
67
+ // the answer held per row, ticking the box on one tab and submitting from
68
+ // another wrote the other tab's stale one
69
+ test('every tab in one dialog shares the answer', () => {
70
+ render(React.createElement(LaunchPlacementProvider, null,
71
+ React.createElement("div", { "data-testid": "tab-a" },
72
+ React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { } })),
73
+ React.createElement("div", { "data-testid": "tab-b" },
74
+ React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { }, submitLabel: "Launch" }))));
75
+ const boxes = screen.getAllByRole('checkbox');
76
+ boxes[0].click();
77
+ expect(boxes[1].checked).toBe(false);
78
+ screen.getByText('Launch').click();
79
+ expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('stack');
50
80
  });
51
81
  test('a stored choice is what the box opens on', () => {
52
82
  localStorage.setItem(LAUNCH_PLACEMENT_KEY, 'stack');
@@ -1,5 +1,17 @@
1
- import React from 'react';
2
- // this is from MUI example
1
+ import React, { useState } from 'react';
2
+ // Once visited, a panel stays mounted and is hidden with the `hidden` attribute.
3
+ // Unmounting it discarded whatever the user had typed and re-ran every fetch the
4
+ // panel makes on the way back, so switching tabs to compare two of them cost an
5
+ // EBI round trip and the pasted alignment.
6
+ //
7
+ // Lazy on first visit rather than mounted up front, because a panel mounts
8
+ // fetches of its own: rendering all four on open would query the MSA dataset
9
+ // adapter and the BLAST cache for tabs nobody looked at.
3
10
  export default function TabPanel({ children, value, index, ...other }) {
4
- return (React.createElement("div", { role: "tabpanel", hidden: value !== index, ...other }, value === index ? children : null));
11
+ const active = value === index;
12
+ const [visited, setVisited] = useState(active);
13
+ if (active && !visited) {
14
+ setVisited(true);
15
+ }
16
+ return (React.createElement("div", { role: "tabpanel", hidden: !active, ...other }, visited || active ? children : null));
5
17
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ // @vitest-environment jsdom
2
+ import React from 'react';
3
+ import { cleanup, render, screen } from '@testing-library/react';
4
+ import { afterEach, expect, test, vi } from 'vitest';
5
+ import TabPanel from './TabPanel';
6
+ afterEach(() => {
7
+ cleanup();
8
+ });
9
+ function Panels({ value }) {
10
+ return (React.createElement(React.Fragment, null,
11
+ React.createElement(TabPanel, { value: value, index: "a" },
12
+ React.createElement(Probe, { name: "a" })),
13
+ React.createElement(TabPanel, { value: value, index: "b" },
14
+ React.createElement(Probe, { name: "b" }))));
15
+ }
16
+ const mounted = vi.fn();
17
+ function Probe({ name }) {
18
+ React.useEffect(() => {
19
+ mounted(name);
20
+ }, [name]);
21
+ return React.createElement("input", { defaultValue: name });
22
+ }
23
+ test('a visited panel keeps its state while another tab is shown', () => {
24
+ const { rerender } = render(React.createElement(Panels, { value: "a" }));
25
+ const typed = screen.getByDisplayValue('a');
26
+ typed.value = 'edited';
27
+ rerender(React.createElement(Panels, { value: "b" }));
28
+ rerender(React.createElement(Panels, { value: "a" }));
29
+ expect(screen.getByDisplayValue('edited')).toBeTruthy();
30
+ expect(mounted.mock.calls.filter(([n]) => n === 'a')).toHaveLength(1);
31
+ });
32
+ test('an unvisited panel does not mount, so it fetches nothing', () => {
33
+ mounted.mockClear();
34
+ render(React.createElement(Panels, { value: "a" }));
35
+ expect(mounted.mock.calls.map(([n]) => n)).toEqual(['a']);
36
+ });
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
+ import type { SequenceStatus } from './SequenceStatus';
2
3
  import type { Feature } from '@jbrowse/core/util';
3
- export default function TranscriptSelector({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, validIds, }: {
4
+ export default function TranscriptSelector({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, sequenceStatus, validIds, }: {
4
5
  feature: Feature;
5
6
  options: Feature[];
6
7
  selectedId: string;
7
8
  selectedTranscript: Feature | undefined;
8
9
  setSelectedId: (transcriptId: string) => void;
9
10
  proteinSequence: string | undefined;
11
+ sequenceStatus?: SequenceStatus;
10
12
  validIds?: string[];
11
13
  }): React.JSX.Element;
@@ -15,7 +15,7 @@ const useStyles = makeStyles()({
15
15
  marginLeft: 20,
16
16
  },
17
17
  });
18
- export default function TranscriptSelector({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, validIds, }) {
18
+ export default function TranscriptSelector({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, sequenceStatus = 'ready', validIds, }) {
19
19
  const { classes } = useStyles();
20
20
  const [showSequence, setShowSequence] = useState(false);
21
21
  return (React.createElement(React.Fragment, null,
@@ -48,5 +48,7 @@ export default function TranscriptSelector({ feature, options, selectedId, selec
48
48
  } }, showSequence ? 'Hide sequence' : 'Show sequence'))),
49
49
  showSequence ? (React.createElement(ReadOnlyTextField2, { value: proteinSequence
50
50
  ? `>${getTranscriptDisplayName(selectedTranscript)}\n${proteinSequence}`
51
- : 'Loading...' })) : null));
51
+ : sequenceStatus === 'missing'
52
+ ? 'This transcript has no coding sequence, so there is nothing to translate.'
53
+ : 'Loading...' })) : null));
52
54
  }
@@ -11,7 +11,10 @@ export declare function revlist(list: Feat[], seqlen: number): {
11
11
  type?: string;
12
12
  phase?: number;
13
13
  }[];
14
- export declare function getProteinSequenceFromFeature({ feature, seq, }: {
14
+ export declare function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCodeId, }: {
15
15
  seq: string;
16
16
  feature: Feature;
17
+ /** the assembly's code for the feature's contig, `{ chrM: 2 }` in hub
18
+ * configs; a transl_table on the feature wins */
19
+ assemblyGeneticCodeId?: number;
17
20
  }): string;
@@ -1,8 +1,9 @@
1
- import { dedupe, revcom } from '@jbrowse/core/util';
1
+ import { revcom } from '@jbrowse/core/util';
2
2
  import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides';
3
- import { getGeneticCode, parseTranslTable } from './geneticCodes';
4
- // `@jbrowse/core/util/convertCodingSequenceToPeptides` is a deep path, so unlike
5
- // the `@jbrowse/core/util` barrel it is absent from ReExports and gets bundled
3
+ import { getGeneticCode, parseTranslTable, } from '@jbrowse/core/util/geneticCodes';
4
+ // `@jbrowse/core/util/convertCodingSequenceToPeptides` and
5
+ // `@jbrowse/core/util/geneticCodes` are deep paths, so unlike the
6
+ // `@jbrowse/core/util` barrel they are absent from ReExports and get bundled
6
7
  // rather than resolved out of the host's JBrowseExports. That is what makes
7
8
  // reusing core's translation safe across every host a config names: this module
8
9
  // previously built its codon table at module scope from the barrel's
@@ -21,6 +22,13 @@ export function calculateProteinSequence({ cds, sequence, geneticCodeId, }) {
21
22
  codonTable,
22
23
  });
23
24
  }
25
+ // The CDS list is sorted by start, so adjacent comparison is the whole job.
26
+ function cdsId(feat) {
27
+ return `${feat.start}-${feat.end}`;
28
+ }
29
+ function dedupe(list) {
30
+ return list.filter((item, pos, ary) => !pos || cdsId(item) !== cdsId(ary[pos - 1]));
31
+ }
24
32
  export function revlist(list, seqlen) {
25
33
  return list
26
34
  .map(sub => ({
@@ -30,7 +38,7 @@ export function revlist(list, seqlen) {
30
38
  }))
31
39
  .toSorted((a, b) => a.start - b.start);
32
40
  }
33
- export function getProteinSequenceFromFeature({ feature, seq, }) {
41
+ export function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCodeId, }) {
34
42
  const { subfeatures, start, strand } = feature.toJSON();
35
43
  const cds = dedupe(subfeatures
36
44
  ?.toSorted((a, b) => a.start - b.start)
@@ -39,15 +47,17 @@ export function getProteinSequenceFromFeature({ feature, seq, }) {
39
47
  start: sub.start - start,
40
48
  end: sub.end - start,
41
49
  }))
42
- .filter(subfeature => subfeature.type === 'CDS') ?? [], feat => `${feat.start}-${feat.end}`);
43
- // a mitochondrial gene declares e.g. transl_table=2, so it translates with
44
- // NCBI table 2 rather than the standard code. GFF3 usually carries the
45
- // attribute on the CDS rather than the transcript, so check both.
50
+ .filter(subfeature => subfeature.type === 'CDS') ?? []);
51
+ // RefSeq declares transl_table=2 on a mitochondrial CDS, usually on the CDS
52
+ // rather than the transcript. GENCODE and UCSC declare nothing, so without
53
+ // the assembly's code all 13 human mitochondrial proteins read TGA as a stop
54
+ // and ATA as I.
46
55
  const cdsSubfeature = feature
47
56
  .get('subfeatures')
48
57
  ?.find((f) => f.get('type')?.toLowerCase() === 'cds');
49
58
  const geneticCodeId = parseTranslTable(feature.get('transl_table')) ??
50
- parseTranslTable(cdsSubfeature?.get('transl_table'));
59
+ parseTranslTable(cdsSubfeature?.get('transl_table')) ??
60
+ assemblyGeneticCodeId;
51
61
  return calculateProteinSequence({
52
62
  cds: strand === -1 ? revlist(cds, seq.length) : cds,
53
63
  sequence: strand === -1 ? revcom(seq) : seq,
@@ -5,4 +5,7 @@ export declare function fetchSeq({ start, end, refName, session, assemblyName, }
5
5
  refName: string;
6
6
  assemblyName: string;
7
7
  session: AbstractSessionModel;
8
- }): Promise<string>;
8
+ }): Promise<{
9
+ seq: string;
10
+ assemblyGeneticCodeId: number | undefined;
11
+ }>;
@@ -6,17 +6,27 @@ export async function fetchSeq({ start, end, refName, session, assemblyName, })
6
6
  throw new Error('assembly not found');
7
7
  }
8
8
  const sessionId = 'getSequence';
9
- const feats = (await rpcManager.call(sessionId, 'CoreGetFeatures', {
9
+ // a named object keeps sessionId, which v4 hosts read from the args
10
+ const args = {
10
11
  adapterConfig: getConf(assembly, ['sequence', 'adapter']),
11
12
  sessionId,
12
13
  regions: [
13
14
  {
14
15
  start,
15
16
  end,
16
- refName: assembly.getCanonicalRefName(refName),
17
+ refName: assembly.getCanonicalRefName(refName) ?? refName,
17
18
  assemblyName,
18
19
  },
19
20
  ],
20
- }));
21
- return feats[0]?.get('seq') ?? '';
21
+ };
22
+ const feats = await rpcManager.call(sessionId, 'CoreGetFeatures', args);
23
+ return {
24
+ seq: feats[0]?.get('seq') ?? '',
25
+ // travels with the sequence because the caller needs both to translate, and
26
+ // resolving the assembly twice to get them is the shape that lost it
27
+ assemblyGeneticCodeId: geneticCodeId(assembly, refName),
28
+ };
29
+ }
30
+ function geneticCodeId(assembly, refName) {
31
+ return assembly.getGeneticCodeId?.(refName);
22
32
  }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ type PlacementState = [boolean, React.Dispatch<React.SetStateAction<boolean>>];
3
+ export declare function LaunchPlacementProvider({ children, }: {
4
+ children: React.ReactNode;
5
+ }): React.JSX.Element;
6
+ /** the dialog's answer, or a private one for a panel rendered outside it */
7
+ export declare function useLaunchPlacement(): PlacementState;
8
+ export {};
@@ -0,0 +1,25 @@
1
+ import React, { createContext, useContext, useState } from 'react';
2
+ import { readLaunchPlacement } from '../../utils/workspaces';
3
+ /**
4
+ * Whether the launch opens beside the genome view — one answer for the whole
5
+ * dialog, not one per tab.
6
+ *
7
+ * Every tab the user visits stays mounted, and the checkbox lives in each
8
+ * panel's actions row, so a `useState` there gave each tab its own copy of the
9
+ * answer: ticking the box on one tab and submitting from another wrote the
10
+ * other tab's stale one.
11
+ *
12
+ * Context rather than a prop through all five panels, none of which has any
13
+ * other business with placement.
14
+ */
15
+ const LaunchPlacementContext = createContext(undefined);
16
+ export function LaunchPlacementProvider({ children, }) {
17
+ const state = useState(() => readLaunchPlacement() === 'splitRight');
18
+ return (React.createElement(LaunchPlacementContext.Provider, { value: state }, children));
19
+ }
20
+ /** the dialog's answer, or a private one for a panel rendered outside it */
21
+ export function useLaunchPlacement() {
22
+ const shared = useContext(LaunchPlacementContext);
23
+ const own = useState(() => readLaunchPlacement() === 'splitRight');
24
+ return shared ?? own;
25
+ }
@@ -9,7 +9,9 @@ export declare function useFeatureSequence({ view, feature, }: {
9
9
  proteinSequence: string;
10
10
  sequence: {
11
11
  seq: string;
12
+ assemblyGeneticCodeId: number | undefined;
12
13
  } | undefined;
13
14
  error: unknown;
15
+ isLoading: boolean;
14
16
  };
15
17
  export {};
@@ -4,25 +4,28 @@ import { getProteinSequenceFromFeature } from './calculateProteinSequence';
4
4
  import { fetchSeq } from './fetchSeq';
5
5
  export function useFeatureSequence({ view, feature, }) {
6
6
  const assemblyName = view?.assemblyNames?.[0];
7
- const { data: sequence, error } = useFetch(feature && assemblyName
7
+ const { data: sequence, error, isLoading, } = useFetch(feature && assemblyName
8
8
  ? [feature.id(), assemblyName, 'feature-sequence']
9
9
  : null, async () => {
10
10
  const { start, end, refName } = feature.toJSON();
11
- return {
12
- seq: await fetchSeq({
13
- start,
14
- end,
15
- refName,
16
- assemblyName: assemblyName,
17
- session: getSession(view),
18
- }),
19
- };
11
+ return fetchSeq({
12
+ start,
13
+ end,
14
+ refName,
15
+ assemblyName: assemblyName,
16
+ session: getSession(view),
17
+ });
20
18
  });
21
19
  return {
22
20
  proteinSequence: sequence && feature
23
- ? getProteinSequenceFromFeature({ seq: sequence.seq, feature })
21
+ ? getProteinSequenceFromFeature({
22
+ seq: sequence.seq,
23
+ feature,
24
+ assemblyGeneticCodeId: sequence.assemblyGeneticCodeId,
25
+ })
24
26
  : '',
25
27
  sequence,
26
28
  error,
29
+ isLoading,
27
30
  };
28
31
  }
@@ -1,10 +1,17 @@
1
+ import type { SequenceStatus } from './SequenceStatus';
1
2
  import type { Feature } from '@jbrowse/core/util';
2
- export declare function useTranscriptSelection({ feature, view, validIds, }: {
3
+ export declare function useTranscriptSelection({ feature, view, validIds, preferredTranscriptId, }: {
3
4
  feature: Feature;
4
5
  view: {
5
6
  assemblyNames?: string[];
6
7
  } | undefined;
7
8
  validIds?: string[];
9
+ /**
10
+ * the isoform the user right-clicked, when the dialog opened on its gene.
11
+ * Ignored when this gene does not carry it, so a stale id falls back to the
12
+ * longest transcript rather than selecting nothing.
13
+ */
14
+ preferredTranscriptId?: string;
8
15
  }): {
9
16
  options: Feature[];
10
17
  selectedId: string;
@@ -12,5 +19,7 @@ export declare function useTranscriptSelection({ feature, view, validIds, }: {
12
19
  selectedTranscript: Feature | undefined;
13
20
  proteinSequence: string;
14
21
  error: unknown;
22
+ isLoading: boolean;
23
+ sequenceStatus: SequenceStatus;
15
24
  validIds: string[] | undefined;
16
25
  };
@@ -16,12 +16,28 @@ function pickSelectedId(currentId, options, validIds) {
16
16
  const firstValid = options.find(isValid);
17
17
  return firstValid ? getId(firstValid) : currentId;
18
18
  }
19
- export function useTranscriptSelection({ feature, view, validIds, }) {
19
+ /**
20
+ * Submit is disabled until the query sequence is in hand, and a grey button
21
+ * reads the same whether the translation is on its way or was never going to
22
+ * arrive. `missing` is the second case: the feature has no CDS to translate,
23
+ * which the dialog never said.
24
+ */
25
+ function sequenceStatus({ error, isLoading, proteinSequence, }) {
26
+ if (error) {
27
+ return 'error';
28
+ }
29
+ if (isLoading) {
30
+ return 'loading';
31
+ }
32
+ return proteinSequence ? 'ready' : 'missing';
33
+ }
34
+ export function useTranscriptSelection({ feature, view, validIds, preferredTranscriptId, }) {
20
35
  const options = useMemo(() => getSortedTranscriptFeatures(feature), [feature]);
21
- const [selectedId, setSelectedId] = useState(() => getId(options[0]));
36
+ const [selectedId, setSelectedId] = useState(() => getId(options.find(opt => featureMatchesId(opt, preferredTranscriptId ?? '')) ??
37
+ options[0]));
22
38
  const validatedSelectedId = pickSelectedId(selectedId, options, validIds);
23
39
  const selectedTranscript = options.find(val => getId(val) === validatedSelectedId);
24
- const { proteinSequence, error } = useFeatureSequence({
40
+ const { proteinSequence, error, isLoading } = useFeatureSequence({
25
41
  view,
26
42
  feature: selectedTranscript,
27
43
  });
@@ -32,6 +48,8 @@ export function useTranscriptSelection({ feature, view, validIds, }) {
32
48
  selectedTranscript,
33
49
  proteinSequence,
34
50
  error,
51
+ isLoading,
52
+ sequenceStatus: sequenceStatus({ error, isLoading, proteinSequence }),
35
53
  validIds,
36
54
  };
37
55
  }
@@ -0,0 +1,57 @@
1
+ // @vitest-environment jsdom
2
+ import { SimpleFeature } from '@jbrowse/core/util';
3
+ import { renderHook } from '@testing-library/react';
4
+ import { describe, expect, test } from 'vitest';
5
+ import { useTranscriptSelection } from './useTranscriptSelection';
6
+ function transcript(id, cdsLength) {
7
+ return {
8
+ uniqueId: id,
9
+ name: id,
10
+ refName: 'chr1',
11
+ start: 0,
12
+ end: cdsLength,
13
+ type: 'mRNA',
14
+ subfeatures: [
15
+ {
16
+ uniqueId: `${id}-cds`,
17
+ refName: 'chr1',
18
+ start: 0,
19
+ end: cdsLength,
20
+ type: 'CDS',
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ // the picker sorts by translated length, so the clicked isoform is deliberately
26
+ // not the one that would be chosen by default
27
+ const gene = new SimpleFeature({
28
+ uniqueId: 'gene1',
29
+ name: 'TP53',
30
+ refName: 'chr1',
31
+ start: 0,
32
+ end: 900,
33
+ type: 'gene',
34
+ subfeatures: [transcript('short', 300), transcript('long', 900)],
35
+ });
36
+ // `view` is left out so useFeatureSequence has no assembly to fetch against and
37
+ // never reaches the session; only the selection is under test here
38
+ function selection(preferredTranscriptId) {
39
+ return renderHook(() => useTranscriptSelection({
40
+ feature: gene,
41
+ view: undefined,
42
+ preferredTranscriptId,
43
+ })).result.current;
44
+ }
45
+ describe('useTranscriptSelection', () => {
46
+ test('opens on the longest transcript with nothing preferred', () => {
47
+ expect(selection().selectedId).toBe('long');
48
+ });
49
+ // climbing to the gene is what puts every isoform in the picker, and it threw
50
+ // away which one the right-click landed on
51
+ test('opens on the isoform the user clicked', () => {
52
+ expect(selection('short').selectedId).toBe('short');
53
+ });
54
+ test('ignores a preferred id this gene does not carry', () => {
55
+ expect(selection('some_other_transcript').selectedId).toBe('long');
56
+ });
57
+ });
@@ -18,6 +18,17 @@ export interface QueryRowMatch {
18
18
  quality: MatchQuality;
19
19
  /** identity over the compared region, 0-1 */
20
20
  identity: number;
21
+ /**
22
+ * Transcript residues before the row's first residue, so transcript position
23
+ * = row position + offset. BLAST reports the aligned region, so a row is
24
+ * often the query with its termini trimmed, and mapping its first residue to
25
+ * the transcript's first residue would put every navigation off by the
26
+ * trimmed amount. Negative when the row is the longer of the two.
27
+ *
28
+ * Zero for a `similar` match, which is the one arm this cannot measure: the
29
+ * row is not the query, so where it starts is a guess.
30
+ */
31
+ offset: number;
21
32
  }
22
33
  export interface MsaQueryRow {
23
34
  /** every row name, in file order, for the picker to offer */
@@ -70,20 +70,27 @@ function bestMatch(parsed, names, query) {
70
70
  }
71
71
  if (row === query) {
72
72
  // nothing beats an exact match, and a second one would be a duplicate row
73
- return { name, quality: 'exact', identity: 1 };
73
+ return { name, quality: 'exact', identity: 1, offset: 0 };
74
74
  }
75
75
  // BLAST reports the aligned region, so the row is often the query trimmed
76
76
  // at one or both ends rather than the whole protein
77
- if (query.includes(row) || row.includes(query)) {
77
+ const contained = query.indexOf(row);
78
+ const containing = row.indexOf(query);
79
+ if (contained !== -1 || containing !== -1) {
78
80
  const coverage = Math.min(row.length, query.length) / Math.max(row.length, query.length);
79
81
  if (coverage >= PARTIAL_COVERAGE_FLOOR) {
80
- candidates.push({ name, quality: 'partial', identity: coverage });
82
+ candidates.push({
83
+ name,
84
+ quality: 'partial',
85
+ identity: coverage,
86
+ offset: contained === -1 ? -containing : contained,
87
+ });
81
88
  }
82
89
  continue;
83
90
  }
84
91
  const identity = identityOverOverlap(row, query);
85
92
  if (identity >= SIMILARITY_FLOOR) {
86
- candidates.push({ name, quality: 'similar', identity });
93
+ candidates.push({ name, quality: 'similar', identity, offset: 0 });
87
94
  }
88
95
  }
89
96
  // an exact match returns above, so only these two can be here
@@ -33,6 +33,36 @@ describe('findQueryRow', () => {
33
33
  quality: 'partial',
34
34
  });
35
35
  });
36
+ // the offset is what the view maps genome coordinates through: without it an
37
+ // N-terminally trimmed row puts every click five codons off, silently
38
+ test('says how far into the protein a trimmed row starts', () => {
39
+ const trimmed = `>aligned_query\n${protein.slice(5, 40)}\n>hit_one\nWRONGWRONGWRONGWRONG\n`;
40
+ expect(findQueryRow(trimmed, protein).match).toMatchObject({
41
+ name: 'aligned_query',
42
+ quality: 'partial',
43
+ offset: 5,
44
+ });
45
+ });
46
+ test('an exact match starts where the protein does', () => {
47
+ expect(findQueryRow(clustal, protein).match?.offset).toBe(0);
48
+ });
49
+ test('a row running past the protein gets a negative offset', () => {
50
+ const extended = `>longer_row\nMMM${protein}\n`;
51
+ expect(findQueryRow(extended, protein).match).toMatchObject({
52
+ name: 'longer_row',
53
+ quality: 'partial',
54
+ offset: -3,
55
+ });
56
+ });
57
+ // the one arm with nothing to measure: a 90%-identical row is not the query,
58
+ // so QueryRowSelector warns rather than claiming a match
59
+ test('a merely similar row claims no offset', () => {
60
+ const similar = protein.slice(0, 30) + 'W' + protein.slice(31);
61
+ expect(findQueryRow(`>near\n${similar}\n`, protein).match).toMatchObject({
62
+ quality: 'similar',
63
+ offset: 0,
64
+ });
65
+ });
36
66
  // the failure that matters: silently wiring the view to a homolog would look
37
67
  // like it worked, and every navigation afterwards would land in the wrong place
38
68
  test('returns nothing when only diverged homologs are present', () => {
@@ -1,5 +1,6 @@
1
1
  import { getContainingTrack, getSession } from '@jbrowse/core/util';
2
2
  import AddIcon from '@mui/icons-material/Add';
3
+ import { isCodingFeature } from './codingFeature';
3
4
  import LaunchMsaViewDialog from './components/LaunchMsaViewDialog';
4
5
  import { launchTarget } from './launchTarget';
5
6
  function isDisplay(elt) {
@@ -9,19 +10,37 @@ function isDisplay(elt) {
9
10
  // built: contextMenuItems runs on every right-click and, on a host whose base
10
11
  // method reads `this`, is the one place a plugin can take the whole menu down.
11
12
  // Keeping it to a pure read of the display is also what lets a test call it.
12
- function openDialog(self, feature) {
13
+ function featureName(feature) {
14
+ return feature.get('name') ?? feature.get('id') ?? 'This feature';
15
+ }
16
+ function openDialog(self, target) {
13
17
  const track = getContainingTrack(self);
14
18
  const session = getSession(track);
15
- feature()
16
- .then(f => {
17
- if (f) {
18
- session.queueDialog(handleClose => [
19
- LaunchMsaViewDialog,
20
- { model: track, handleClose, feature: f },
21
- ]);
19
+ const { preferredTranscriptId } = target;
20
+ const open = (feature) => {
21
+ session.queueDialog(handleClose => [
22
+ LaunchMsaViewDialog,
23
+ { model: track, handleClose, feature, preferredTranscriptId },
24
+ ]);
25
+ };
26
+ if ('feature' in target) {
27
+ open(target.feature);
28
+ return;
29
+ }
30
+ target
31
+ .fetchFeature()
32
+ .then(feature => {
33
+ if (!feature) {
34
+ session.notify('Could not load feature for MSA view', 'warning');
35
+ }
36
+ else if (!isCodingFeature(feature)) {
37
+ // the canvas hit test carries a type and nothing else, so this is the
38
+ // first point at which the CDS can be looked for. Saying so beats
39
+ // opening a dialog whose Submit never leaves grey.
40
+ session.notify(`${featureName(feature)} has no coding sequence, so there is no protein to align`, 'info');
22
41
  }
23
42
  else {
24
- session.notify('Could not load feature for MSA view', 'warning');
43
+ open(feature);
25
44
  }
26
45
  })
27
46
  .catch((e) => {