jbrowse-plugin-msaview 3.5.0 → 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 (130) hide show
  1. package/dist/AddHighlightModel/index.js +3 -3
  2. package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.d.ts +8 -2
  3. package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +3 -3
  4. package/dist/BgzipFastaMsaAdapter/configSchema.js +1 -1
  5. package/dist/LaunchMsaView/codingFeature.d.ts +14 -0
  6. package/dist/LaunchMsaView/codingFeature.js +38 -0
  7. package/dist/LaunchMsaView/codingFeature.test.d.ts +1 -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 +19 -24
  11. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.d.ts +3 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +6 -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/consts.d.ts +2 -0
  17. package/dist/LaunchMsaView/components/BlastQuery/consts.js +28 -4
  18. package/dist/LaunchMsaView/components/HelpButton.d.ts +2 -0
  19. package/dist/LaunchMsaView/components/HelpButton.js +17 -0
  20. package/dist/LaunchMsaView/components/HelpDialog.d.ts +4 -0
  21. package/dist/LaunchMsaView/components/HelpDialog.js +18 -0
  22. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.d.ts +3 -1
  23. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +21 -16
  24. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.d.ts +3 -1
  25. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +9 -4
  26. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +3 -1
  27. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +17 -8
  28. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.d.ts +3 -1
  29. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +33 -15
  30. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +3 -1
  31. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +2 -1
  32. package/dist/LaunchMsaView/components/SequenceStatus.d.ts +14 -0
  33. package/dist/LaunchMsaView/components/SequenceStatus.js +15 -0
  34. package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
  35. package/dist/LaunchMsaView/components/SubmitCancelActions.js +25 -13
  36. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +33 -3
  37. package/dist/LaunchMsaView/components/TabPanel.js +15 -3
  38. package/dist/LaunchMsaView/components/TabPanel.test.d.ts +1 -0
  39. package/dist/LaunchMsaView/components/TabPanel.test.js +36 -0
  40. package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +3 -1
  41. package/dist/LaunchMsaView/components/TranscriptSelector.js +4 -2
  42. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +4 -1
  43. package/dist/LaunchMsaView/components/calculateProteinSequence.js +20 -10
  44. package/dist/LaunchMsaView/components/fetchSeq.d.ts +4 -1
  45. package/dist/LaunchMsaView/components/fetchSeq.js +14 -4
  46. package/dist/LaunchMsaView/components/launchPlacement.d.ts +8 -0
  47. package/dist/LaunchMsaView/components/launchPlacement.js +25 -0
  48. package/dist/LaunchMsaView/components/useFeatureSequence.d.ts +2 -0
  49. package/dist/LaunchMsaView/components/useFeatureSequence.js +14 -11
  50. package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +10 -1
  51. package/dist/LaunchMsaView/components/useTranscriptSelection.js +21 -3
  52. package/dist/LaunchMsaView/components/useTranscriptSelection.test.d.ts +1 -0
  53. package/dist/LaunchMsaView/components/useTranscriptSelection.test.js +57 -0
  54. package/dist/LaunchMsaView/index.js +28 -9
  55. package/dist/LaunchMsaView/launchTarget.d.ts +36 -8
  56. package/dist/LaunchMsaView/launchTarget.js +23 -13
  57. package/dist/LaunchMsaView/launchTarget.test.js +80 -15
  58. package/dist/LaunchMsaView/useQueryRowName.d.ts +12 -0
  59. package/dist/LaunchMsaView/useQueryRowName.js +11 -0
  60. package/dist/LaunchMsaView/useQueryRowName.test.js +18 -1
  61. package/dist/LaunchMsaView/util.js +5 -2
  62. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +6 -4
  63. package/dist/MsaViewPanel/afterCreateAutoruns.js +26 -11
  64. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +4 -3
  65. package/dist/MsaViewPanel/components/LaunchProgress.js +10 -7
  66. package/dist/MsaViewPanel/components/MsaViewPanel.js +7 -3
  67. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +24 -0
  68. package/dist/MsaViewPanel/genomeToMSA.js +21 -3
  69. package/dist/MsaViewPanel/genomeToMSA.test.js +153 -25
  70. package/dist/MsaViewPanel/model.d.ts +143 -52
  71. package/dist/MsaViewPanel/model.js +52 -3
  72. package/dist/MsaViewPanel/model.test.js +66 -0
  73. package/dist/MsaViewPanel/resolveConnectedTranscript.js +10 -4
  74. package/dist/MsaViewPanel/storedData.test.js +20 -0
  75. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +12 -5
  76. package/dist/index.d.ts +11 -11
  77. package/dist/jbrowse-plugin-msaview.umd.production.min.js +54 -67
  78. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  79. package/dist/version.d.ts +1 -1
  80. package/dist/version.js +1 -1
  81. package/package.json +21 -21
  82. package/src/AddHighlightModel/index.tsx +1 -1
  83. package/src/BgzipFastaMsaAdapter/configSchema.ts +1 -1
  84. package/src/LaunchMsaView/codingFeature.test.ts +96 -0
  85. package/src/LaunchMsaView/codingFeature.ts +49 -0
  86. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +25 -24
  87. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +8 -1
  88. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +24 -11
  89. package/src/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.tsx +4 -4
  90. package/src/LaunchMsaView/components/BlastQuery/consts.ts +31 -4
  91. package/src/LaunchMsaView/components/HelpButton.tsx +33 -0
  92. package/src/LaunchMsaView/components/HelpDialog.tsx +78 -0
  93. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +54 -34
  94. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +12 -2
  95. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +23 -7
  96. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +63 -13
  97. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +4 -0
  98. package/src/LaunchMsaView/components/SequenceStatus.tsx +29 -0
  99. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +62 -3
  100. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +37 -15
  101. package/src/LaunchMsaView/components/TabPanel.test.tsx +51 -0
  102. package/src/LaunchMsaView/components/TabPanel.tsx +16 -4
  103. package/src/LaunchMsaView/components/TranscriptSelector.tsx +6 -1
  104. package/src/LaunchMsaView/components/calculateProteinSequence.ts +29 -10
  105. package/src/LaunchMsaView/components/fetchSeq.ts +25 -5
  106. package/src/LaunchMsaView/components/launchPlacement.tsx +41 -0
  107. package/src/LaunchMsaView/components/useFeatureSequence.ts +18 -11
  108. package/src/LaunchMsaView/components/useTranscriptSelection.test.tsx +66 -0
  109. package/src/LaunchMsaView/components/useTranscriptSelection.ts +41 -2
  110. package/src/LaunchMsaView/index.ts +32 -13
  111. package/src/LaunchMsaView/launchTarget.test.ts +88 -15
  112. package/src/LaunchMsaView/launchTarget.ts +49 -14
  113. package/src/LaunchMsaView/useQueryRowName.test.ts +28 -1
  114. package/src/LaunchMsaView/useQueryRowName.ts +17 -0
  115. package/src/LaunchMsaView/util.ts +8 -4
  116. package/src/MsaViewPanel/afterCreateAutoruns.ts +34 -11
  117. package/src/MsaViewPanel/components/LaunchProgress.tsx +12 -5
  118. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +26 -0
  119. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -5
  120. package/src/MsaViewPanel/genomeToMSA.test.ts +184 -40
  121. package/src/MsaViewPanel/genomeToMSA.ts +27 -3
  122. package/src/MsaViewPanel/model.test.ts +75 -0
  123. package/src/MsaViewPanel/model.ts +54 -3
  124. package/src/MsaViewPanel/resolveConnectedTranscript.ts +14 -4
  125. package/src/MsaViewPanel/storedData.test.ts +26 -0
  126. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +14 -5
  127. package/src/version.ts +1 -1
  128. package/dist/LaunchMsaView/components/geneticCodes.d.ts +0 -15
  129. package/dist/LaunchMsaView/components/geneticCodes.js +0 -227
  130. package/src/LaunchMsaView/components/geneticCodes.ts +0 -298
@@ -8,6 +8,7 @@ import { useQueryRowName } from '../../useQueryRowName';
8
8
  import { getGeneDisplayName, getLinearGenomeView } from '../../util';
9
9
  import LaunchPanelContent from '../LaunchPanelContent';
10
10
  import QueryRowSelector from '../QueryRowSelector';
11
+ import SequenceStatusMessage from '../SequenceStatus';
11
12
  import SubmitCancelActions from '../SubmitCancelActions';
12
13
  import TranscriptSelector from '../TranscriptSelector';
13
14
  import { useTranscriptSelection } from '../useTranscriptSelection';
@@ -26,7 +27,7 @@ const useStyles = makeStyles()({
26
27
  marginBottom: 20,
27
28
  },
28
29
  });
29
- const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handleClose, }) {
30
+ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handleClose, preferredTranscriptId, }) {
30
31
  const view = getLinearGenomeView(model);
31
32
  const { classes } = useStyles();
32
33
  const [launchViewError, setLaunchViewError] = useState();
@@ -35,8 +36,12 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
35
36
  const [treeText, setTreeText] = useState('');
36
37
  const [msaFileLocation, setMsaFileLocation] = useState();
37
38
  const [treeFileLocation, setTreeFileLocation] = useState();
38
- const transcriptSelection = useTranscriptSelection({ feature, view });
39
- const { selectedTranscript, proteinSequence, error } = transcriptSelection;
39
+ const transcriptSelection = useTranscriptSelection({
40
+ feature,
41
+ view,
42
+ preferredTranscriptId,
43
+ });
44
+ const { selectedTranscript, proteinSequence, error, sequenceStatus } = transcriptSelection;
40
45
  const queryRow = useQueryRowName(msaText, proteinSequence);
41
46
  const e = launchViewError ?? error;
42
47
  return (React.createElement(React.Fragment, null,
@@ -58,7 +63,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
58
63
  } })))),
59
64
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
60
65
  React.createElement(QueryRowSelector, { ...queryRow })),
61
- React.createElement(SubmitCancelActions, { model: model, submitDisabled: !selectedTranscript ||
66
+ React.createElement(SubmitCancelActions, { model: model, hint: React.createElement(SequenceStatusMessage, { status: sequenceStatus }), submitDisabled: !selectedTranscript ||
62
67
  (inputMethod === 'file' && !msaFileLocation) ||
63
68
  (inputMethod === 'text' && !msaText.trim()), onSubmit: () => {
64
69
  try {
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
2
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util';
3
- declare const OrthologPanel: ({ handleClose, feature, model, }: {
3
+ declare const OrthologPanel: ({ handleClose, feature, model, preferredTranscriptId, }: {
4
4
  model: AbstractTrackModel;
5
5
  feature: Feature;
6
6
  handleClose: () => void;
7
+ /** the isoform the user right-clicked, preselected in the picker */
8
+ preferredTranscriptId?: string;
7
9
  }) => React.JSX.Element;
8
10
  export default OrthologPanel;
@@ -9,6 +9,7 @@ import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscr
9
9
  import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect';
10
10
  import { useStoredMsaAlgorithm } from '../BlastQuery/searchChoiceStorage';
11
11
  import LaunchPanelContent from '../LaunchPanelContent';
12
+ import SequenceStatusMessage from '../SequenceStatus';
12
13
  import SubmitCancelActions from '../SubmitCancelActions';
13
14
  import TranscriptSelector from '../TranscriptSelector';
14
15
  import { useTranscriptSelection } from '../useTranscriptSelection';
@@ -20,12 +21,14 @@ const useStyles = makeStyles()({
20
21
  width: 180,
21
22
  },
22
23
  });
24
+ // the N was literal: the helper text said "the closest N species" whatever the
25
+ // box held
23
26
  const rowsHint = {
24
- ncbi: 'the closest N species NCBI has',
25
- panther: 'the closest N species PANTHER has',
26
- uniref: 'one per species, reviewed entries first',
27
+ ncbi: rows => `the ${rows} closest species NCBI has`,
28
+ panther: rows => `the ${rows} closest species PANTHER has`,
29
+ uniref: rows => `${rows} rows, one per species, reviewed entries first`,
27
30
  };
28
- const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
31
+ const OrthologPanel = observer(function ({ handleClose, feature, model, preferredTranscriptId, }) {
29
32
  const { classes } = useStyles();
30
33
  const view = getLinearGenomeView(model);
31
34
  const [launchViewError, setLaunchViewError] = useState();
@@ -34,8 +37,12 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
34
37
  const [msaAlgorithm, setMsaAlgorithm] = useStoredMsaAlgorithm();
35
38
  const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies));
36
39
  const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
37
- const transcriptSelection = useTranscriptSelection({ feature, view });
38
- const { selectedTranscript, proteinSequence } = transcriptSelection;
40
+ const transcriptSelection = useTranscriptSelection({
41
+ feature,
42
+ view,
43
+ preferredTranscriptId,
44
+ });
45
+ const { selectedTranscript, proteinSequence, sequenceStatus } = transcriptSelection;
39
46
  const e = transcriptSelection.error ?? launchViewError;
40
47
  const rowCount = Number(maxSpecies);
41
48
  const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2;
@@ -48,9 +55,11 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
48
55
  React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: msaAlgorithm, onChange: setMsaAlgorithm }),
49
56
  React.createElement(TextField2, { variant: "outlined", label: "Rows to align", className: classes.selectField, type: "number", value: maxSpecies, onChange: event => {
50
57
  setMaxSpecies(event.target.value);
51
- }, error: !rowCountValid, helperText: rowsHint[source] })),
58
+ }, error: !rowCountValid, helperText: rowCountValid
59
+ ? rowsHint[source](rowCount)
60
+ : 'a whole number, 2 or more' })),
52
61
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection })),
53
- React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
62
+ React.createElement(SubmitCancelActions, { model: model, hint: React.createElement(SequenceStatusMessage, { status: sequenceStatus }), submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
54
63
  try {
55
64
  if (selectedTranscript) {
56
65
  setLaunchViewError(undefined);
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
2
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util';
3
- declare const PreLoadedMSA: ({ model, feature, handleClose, }: {
3
+ declare const PreLoadedMSA: ({ model, feature, handleClose, preferredTranscriptId, }: {
4
4
  model: AbstractTrackModel;
5
5
  feature: Feature;
6
6
  handleClose: () => void;
7
+ /** the isoform the user right-clicked, preselected in the picker */
8
+ preferredTranscriptId?: string;
7
9
  }) => React.JSX.Element;
8
10
  export default PreLoadedMSA;
@@ -1,13 +1,16 @@
1
- import React, { useState } from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
  import { LoadingEllipses, SanitizedHTML } from '@jbrowse/core/ui';
3
3
  import { getEnv, getSession } from '@jbrowse/core/util';
4
- import { MenuItem } from '@mui/material';
4
+ import { MenuItem, Typography } from '@mui/material';
5
5
  import { observer } from 'mobx-react';
6
6
  import { makeStyles } from 'tss-react/mui';
7
7
  import TextField2 from '../../../components/TextField2';
8
8
  import { useFetch } from '../../../utils/useFetch';
9
- import { getGeneDisplayName, getLinearGenomeView } from '../../util';
9
+ import { resolveQueryRowName, useQueryRowName } from '../../useQueryRowName';
10
+ import { getGeneDisplayName, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
10
11
  import LaunchPanelContent from '../LaunchPanelContent';
12
+ import QueryRowSelector from '../QueryRowSelector';
13
+ import SequenceStatusMessage from '../SequenceStatus';
11
14
  import SubmitCancelActions from '../SubmitCancelActions';
12
15
  import TranscriptSelector from '../TranscriptSelector';
13
16
  import { useTranscriptSelection } from '../useTranscriptSelection';
@@ -19,7 +22,7 @@ const useStyles = makeStyles()({
19
22
  marginTop: 50,
20
23
  },
21
24
  });
22
- const PreLoadedMSA = observer(function ({ model, feature, handleClose, }) {
25
+ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferredTranscriptId, }) {
23
26
  const session = getSession(model);
24
27
  const view = getLinearGenomeView(model);
25
28
  const { classes } = useStyles();
@@ -34,8 +37,9 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, }) {
34
37
  feature,
35
38
  view,
36
39
  validIds: msaList,
40
+ preferredTranscriptId,
37
41
  });
38
- const { selectedId, selectedTranscript } = transcriptSelection;
42
+ const { selectedId, selectedTranscript, proteinSequence, sequenceStatus } = transcriptSelection;
39
43
  const { data: msaData, isLoading: msaDataLoading, error: msaDataFetchError, } = useFetch(selectedId && selectedDataset && msaList
40
44
  ? `${selectedDataset.datasetId}-${selectedId}-msa`
41
45
  : null, () => fetchMSA({
@@ -43,6 +47,19 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, }) {
43
47
  config: selectedDataset.adapter,
44
48
  pluginManager,
45
49
  }));
50
+ const msaText = useMemo(() => msaData?.map(r => `>${r.get('refName')}\n${r.get('seq')}`).join('\n') ??
51
+ '', [msaData]);
52
+ // The dataset's row for this transcript used to be assumed -- the launch
53
+ // named `<transcriptId>_<assembly>` and hoped the file agreed. Nothing
54
+ // checked, and a name the alignment does not carry fails silently: the view
55
+ // opens, renders, and never navigates. The row is found by sequence instead,
56
+ // the same way the Manual tab finds it.
57
+ const queryRow = useQueryRowName(msaText, proteinSequence);
58
+ // The name this panel used to launch with unconditionally. A dataset built to
59
+ // that convention does carry the row, so it is worth falling back to when the
60
+ // residues do not match closely enough to find it -- but only when the
61
+ // alignment really has it, which is the check the old code never made.
62
+ const querySeqName = resolveQueryRowName(queryRow, `${selectedId}_${assemblyNames[0] ?? ''}`);
46
63
  const e = msaListFetchError ??
47
64
  msaDataFetchError ??
48
65
  transcriptSelection.error ??
@@ -53,25 +70,26 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, }) {
53
70
  setSelectedDatasetId(event.target.value);
54
71
  } }, datasets?.map(d => (React.createElement(MenuItem, { key: d.datasetId, value: d.datasetId }, d.name)))),
55
72
  selectedDataset ? (React.createElement("div", { className: classes.selectedContainer },
56
- !msaListLoading && msaDataLoading ? (React.createElement(LoadingEllipses, { variant: "h6", message: `Loading MSA for (${selectedId})` })) : null,
73
+ !msaListLoading && msaDataLoading ? (React.createElement(LoadingEllipses, { variant: "h6", message: `Loading MSA for ${getTranscriptDisplayName(selectedTranscript) || selectedId}` })) : null,
57
74
  msaListLoading ? (React.createElement(LoadingEllipses, { variant: "h6", message: `Loading available MSAs for (${selectedDataset.name})` })) : null,
58
75
  msaList ? (React.createElement("div", null,
59
76
  React.createElement(SanitizedHTML, { html: selectedDataset.description }),
60
- React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }))) : null)) : null),
61
- React.createElement(SubmitCancelActions, { model: model, submitDisabled: !selectedTranscript || !msaData?.length, onSubmit: () => {
77
+ React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
78
+ msaText ? (React.createElement(QueryRowSelector, { ...queryRow, querySeqName: querySeqName })) : null)) : null)) : null),
79
+ React.createElement(SubmitCancelActions, { model: model, hint: msaDataLoading ? (React.createElement(LoadingEllipses, { message: "Loading alignment" })) : !!msaData?.length && !querySeqName ? (React.createElement(Typography, { color: "textSecondary", variant: "body2" }, "no row matches this transcript")) : (React.createElement(SequenceStatusMessage, { status: sequenceStatus })),
80
+ // launching without a query row opens a view that renders and then
81
+ // never navigates, which reads as a broken feature rather than a
82
+ // dataset that does not cover this gene
83
+ submitDisabled: !selectedTranscript || !msaData?.length || !querySeqName, onSubmit: () => {
62
84
  try {
63
- if (selectedTranscript && msaData) {
64
- const querySeqName = `${selectedId}_${assemblyNames[0]}`;
85
+ if (selectedTranscript && msaText) {
65
86
  preCalculatedLaunchView({
66
87
  newViewTitle: getGeneDisplayName(selectedTranscript),
67
88
  view,
68
89
  querySeqName,
90
+ querySeqOffset: queryRow.querySeqOffset,
69
91
  feature: selectedTranscript,
70
- data: {
71
- msa: msaData
72
- .map(r => `>${r.get('refName')}\n${r.get('seq')}`)
73
- .join('\n'),
74
- },
92
+ data: { msa: msaText },
75
93
  });
76
94
  handleClose();
77
95
  }
@@ -1,6 +1,6 @@
1
1
  import type { Feature } from '@jbrowse/core/util';
2
2
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
3
- export declare function preCalculatedLaunchView({ newViewTitle, view, feature, data, querySeqName, }: {
3
+ export declare function preCalculatedLaunchView({ newViewTitle, view, feature, data, querySeqName, querySeqOffset, }: {
4
4
  data: {
5
5
  msa: string;
6
6
  };
@@ -8,4 +8,6 @@ export declare function preCalculatedLaunchView({ newViewTitle, view, feature, d
8
8
  view: LinearGenomeViewModel;
9
9
  feature: Feature;
10
10
  querySeqName: string;
11
+ /** transcript residues before the query row's first residue */
12
+ querySeqOffset?: number;
11
13
  }): void;
@@ -1,12 +1,13 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
2
  import { launchMsaView } from '../../../utils/launchMsaView';
3
3
  import { readLaunchPlacement } from '../../../utils/workspaces';
4
- export function preCalculatedLaunchView({ newViewTitle, view, feature, data, querySeqName, }) {
4
+ export function preCalculatedLaunchView({ newViewTitle, view, feature, data, querySeqName, querySeqOffset, }) {
5
5
  launchMsaView(getSession(view), {
6
6
  placement: readLaunchPlacement(),
7
7
  displayName: newViewTitle,
8
8
  treeAreaWidth: 200,
9
9
  querySeqName,
10
+ querySeqOffset,
10
11
  treeWidth: 100,
11
12
  drawNodeBubbles: false,
12
13
  labelsAlignRight: true,
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ export type SequenceStatus = 'loading' | 'ready' | 'missing' | 'error';
3
+ /**
4
+ * Why Submit is grey. The query row is the selected transcript's translation,
5
+ * and fetching and translating it takes a round trip to the sequence adapter,
6
+ * so the button starts disabled on every panel — with nothing to distinguish
7
+ * "wait a moment" from "this gene has no protein and never will".
8
+ *
9
+ * An error says so through the panel's own ErrorMessage, so this stays quiet
10
+ * for that one rather than saying it twice.
11
+ */
12
+ export default function SequenceStatusMessage({ status, }: {
13
+ status: SequenceStatus;
14
+ }): React.JSX.Element | null;
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { LoadingEllipses } from '@jbrowse/core/ui';
3
+ import { Typography } from '@mui/material';
4
+ /**
5
+ * Why Submit is grey. The query row is the selected transcript's translation,
6
+ * and fetching and translating it takes a round trip to the sequence adapter,
7
+ * so the button starts disabled on every panel — with nothing to distinguish
8
+ * "wait a moment" from "this gene has no protein and never will".
9
+ *
10
+ * An error says so through the panel's own ErrorMessage, so this stays quiet
11
+ * for that one rather than saying it twice.
12
+ */
13
+ export default function SequenceStatusMessage({ status, }) {
14
+ return status === 'loading' ? (React.createElement(LoadingEllipses, { message: "Translating transcript" })) : status === 'missing' ? (React.createElement(Typography, { color: "textSecondary", variant: "body2" }, "no coding sequence to align")) : null;
15
+ }
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import type { AbstractTrackModel } from '@jbrowse/core/util';
3
- export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, submitLabel, cancelLabel, model, }: {
3
+ export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, hint, submitLabel, cancelLabel, model, }: {
4
4
  onSubmit: () => void;
5
5
  onCancel: () => void;
6
6
  submitDisabled?: boolean;
7
+ /** why Submit is grey, shown beside it */
8
+ hint?: React.ReactNode;
7
9
  submitLabel?: string;
8
10
  cancelLabel?: string;
9
11
  /** omitted by a panel that submits something other than a view launch */
@@ -1,7 +1,8 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
3
  import { Button, Checkbox, DialogActions, FormControlLabel, } from '@mui/material';
4
- import { readLaunchPlacement, sessionSupportsPlacement, writeLaunchPlacement, } from '../../utils/workspaces';
4
+ import { sessionSupportsPlacement, writeLaunchPlacement, } from '../../utils/workspaces';
5
+ import { useLaunchPlacement } from './launchPlacement';
5
6
  /**
6
7
  * Where the launch puts the view, offered wherever a launch is submitted.
7
8
  *
@@ -13,20 +14,31 @@ import { readLaunchPlacement, sessionSupportsPlacement, writeLaunchPlacement, }
13
14
  * release that places views its own way — because the box would do nothing
14
15
  * there and every launch would quietly ignore it.
15
16
  */
16
- function PlacementToggle({ model }) {
17
- const session = getSession(model);
18
- const [sideBySide, setSideBySide] = useState(() => readLaunchPlacement() === 'splitRight');
19
- return sessionSupportsPlacement(session) ? (React.createElement(FormControlLabel, { label: "Open beside the genome view", control: React.createElement(Checkbox, { checked: sideBySide, onChange: event => {
20
- const { checked } = event.target;
21
- setSideBySide(checked);
22
- writeLaunchPlacement(checked ? 'splitRight' : 'stack');
23
- } }) })) : null;
17
+ function PlacementToggle({ checked, onChange, }) {
18
+ return (React.createElement(FormControlLabel, { label: "Open beside the genome view", control: React.createElement(Checkbox, { checked: checked, onChange: event => {
19
+ onChange(event.target.checked);
20
+ } }) }));
24
21
  }
25
- export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, submitLabel = 'Submit', cancelLabel = 'Cancel', model, }) {
22
+ export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, hint, submitLabel = 'Submit', cancelLabel = 'Cancel', model, }) {
23
+ const [sideBySide, setSideBySide] = useLaunchPlacement();
24
+ // The stored value is what the next launch reads, so it is written on submit
25
+ // rather than on the click: ticking the box and then pressing Cancel used to
26
+ // change where every future launch landed, from a dialog the user backed out
27
+ // of.
28
+ const offerPlacement = !!model && sessionSupportsPlacement(getSession(model));
26
29
  return (React.createElement(DialogActions, { sx: { flexWrap: 'wrap', rowGap: 1 } },
27
- model ? React.createElement(PlacementToggle, { model: model }) : null,
28
- React.createElement("div", { style: { display: 'flex', gap: 8, marginLeft: 'auto' } },
30
+ offerPlacement ? (React.createElement(PlacementToggle, { checked: sideBySide, onChange: setSideBySide })) : null,
31
+ React.createElement("div", { style: {
32
+ display: 'flex',
33
+ alignItems: 'center',
34
+ gap: 8,
35
+ marginLeft: 'auto',
36
+ } },
37
+ hint,
29
38
  React.createElement(Button, { sx: { flexShrink: 0 }, color: "primary", variant: "contained", disabled: submitDisabled, onClick: () => {
39
+ if (offerPlacement) {
40
+ writeLaunchPlacement(sideBySide ? 'splitRight' : 'stack');
41
+ }
30
42
  onSubmit();
31
43
  } }, submitLabel),
32
44
  React.createElement(Button, { sx: { flexShrink: 0 }, color: "secondary", variant: "contained", onClick: () => {
@@ -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 {};