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.
- package/README.md +7 -0
- package/dist/AddHighlightModel/index.js +3 -3
- package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.d.ts +8 -2
- package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +3 -3
- package/dist/BgzipFastaMsaAdapter/configSchema.js +1 -1
- package/dist/LaunchMsaView/codingFeature.d.ts +14 -0
- package/dist/LaunchMsaView/codingFeature.js +38 -0
- package/dist/LaunchMsaView/codingFeature.test.js +75 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.d.ts +3 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -26
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.d.ts +3 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +7 -2
- package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.d.ts +7 -3
- package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +8 -7
- package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.js +2 -2
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +26 -4
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +68 -2
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +1 -1
- package/dist/LaunchMsaView/components/HelpButton.d.ts +2 -0
- package/dist/LaunchMsaView/components/HelpButton.js +17 -0
- package/dist/LaunchMsaView/components/HelpDialog.d.ts +4 -0
- package/dist/LaunchMsaView/components/HelpDialog.js +18 -0
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.d.ts +3 -1
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +21 -16
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.d.ts +3 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +10 -4
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +3 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +21 -7
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.d.ts +3 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +33 -15
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +3 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +2 -1
- package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
- package/dist/LaunchMsaView/components/SequenceStatus.d.ts +14 -0
- package/dist/LaunchMsaView/components/SequenceStatus.js +15 -0
- package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
- package/dist/LaunchMsaView/components/SubmitCancelActions.js +25 -13
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +33 -3
- package/dist/LaunchMsaView/components/TabPanel.js +15 -3
- package/dist/LaunchMsaView/components/TabPanel.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/TabPanel.test.js +36 -0
- package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +3 -1
- package/dist/LaunchMsaView/components/TranscriptSelector.js +4 -2
- package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +4 -1
- package/dist/LaunchMsaView/components/calculateProteinSequence.js +20 -10
- package/dist/LaunchMsaView/components/fetchSeq.d.ts +4 -1
- package/dist/LaunchMsaView/components/fetchSeq.js +14 -4
- package/dist/LaunchMsaView/components/launchPlacement.d.ts +8 -0
- package/dist/LaunchMsaView/components/launchPlacement.js +25 -0
- package/dist/LaunchMsaView/components/useFeatureSequence.d.ts +2 -0
- package/dist/LaunchMsaView/components/useFeatureSequence.js +14 -11
- package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +10 -1
- package/dist/LaunchMsaView/components/useTranscriptSelection.js +21 -3
- package/dist/LaunchMsaView/components/useTranscriptSelection.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/useTranscriptSelection.test.js +57 -0
- package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
- package/dist/LaunchMsaView/detectQueryRow.js +11 -4
- package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
- package/dist/LaunchMsaView/index.js +28 -9
- package/dist/LaunchMsaView/launchTarget.d.ts +36 -8
- package/dist/LaunchMsaView/launchTarget.js +23 -13
- package/dist/LaunchMsaView/launchTarget.test.js +80 -15
- package/dist/LaunchMsaView/useQueryRowName.d.ts +13 -0
- package/dist/LaunchMsaView/useQueryRowName.js +16 -1
- package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.test.js +40 -0
- package/dist/LaunchMsaView/util.js +5 -2
- package/dist/LaunchMsaViewExtensionPoint/index.js +8 -6
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +21 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +9 -6
- package/dist/MsaViewPanel/afterCreateAutoruns.js +72 -28
- package/dist/MsaViewPanel/components/LaunchProgress.d.ts +4 -3
- package/dist/MsaViewPanel/components/LaunchProgress.js +22 -10
- package/dist/MsaViewPanel/components/MsaViewPanel.js +10 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +47 -2
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +8 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +78 -93
- package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.js +40 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +10 -0
- package/dist/MsaViewPanel/genomeToMSA.js +25 -7
- package/dist/MsaViewPanel/genomeToMSA.test.js +214 -19
- package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
- package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
- package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
- package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
- package/dist/MsaViewPanel/model.d.ts +265 -103
- package/dist/MsaViewPanel/model.js +174 -48
- package/dist/MsaViewPanel/model.test.d.ts +1 -0
- package/dist/MsaViewPanel/model.test.js +166 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
- package/dist/MsaViewPanel/msaDataStore.d.ts +2 -0
- package/dist/MsaViewPanel/msaDataStore.js +10 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
- package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
- package/dist/MsaViewPanel/processInit.test.js +72 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +128 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.js +35 -0
- package/dist/MsaViewPanel/runLaunch.test.js +46 -0
- package/dist/MsaViewPanel/storedData.test.js +20 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
- package/dist/MsaViewPanel/structureConnection.js +0 -19
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +12 -5
- package/dist/MsaViewPanel/util.d.ts +39 -0
- package/dist/MsaViewPanel/util.js +44 -0
- package/dist/index.d.ts +11 -11
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +56 -68
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/browserAlign.d.ts +42 -0
- package/dist/utils/browserAlign.js +297 -0
- package/dist/utils/browserAlign.test.d.ts +1 -0
- package/dist/utils/browserAlign.test.js +85 -0
- package/dist/utils/ebiBlast.d.ts +10 -1
- package/dist/utils/ebiBlast.js +27 -1
- package/dist/utils/homologSearch.d.ts +31 -0
- package/dist/utils/homologSearch.js +6 -0
- package/dist/utils/msa.d.ts +3 -11
- package/dist/utils/msa.js +14 -29
- package/dist/utils/msaRows.d.ts +11 -8
- package/dist/utils/msaRows.js +14 -12
- package/dist/utils/phmmer.d.ts +11 -1
- package/dist/utils/phmmer.js +49 -12
- package/dist/utils/unirefHomologs.d.ts +94 -0
- package/dist/utils/unirefHomologs.js +193 -0
- package/dist/utils/unirefHomologs.test.d.ts +1 -0
- package/dist/utils/unirefHomologs.test.js +118 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +23 -22
- package/src/AddHighlightModel/index.tsx +1 -1
- package/src/BgzipFastaMsaAdapter/configSchema.ts +1 -1
- package/src/LaunchMsaView/codingFeature.test.ts +96 -0
- package/src/LaunchMsaView/codingFeature.ts +49 -0
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +52 -26
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +9 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +24 -11
- package/src/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.tsx +4 -4
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +77 -2
- package/src/LaunchMsaView/components/HelpButton.tsx +33 -0
- package/src/LaunchMsaView/components/HelpDialog.tsx +78 -0
- package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +54 -34
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +13 -2
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +28 -5
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +63 -13
- package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +4 -0
- package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
- package/src/LaunchMsaView/components/SequenceStatus.tsx +29 -0
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +62 -3
- package/src/LaunchMsaView/components/SubmitCancelActions.tsx +37 -15
- package/src/LaunchMsaView/components/TabPanel.test.tsx +51 -0
- package/src/LaunchMsaView/components/TabPanel.tsx +16 -4
- package/src/LaunchMsaView/components/TranscriptSelector.tsx +6 -1
- package/src/LaunchMsaView/components/calculateProteinSequence.ts +29 -10
- package/src/LaunchMsaView/components/fetchSeq.ts +25 -5
- package/src/LaunchMsaView/components/launchPlacement.tsx +41 -0
- package/src/LaunchMsaView/components/useFeatureSequence.ts +18 -11
- package/src/LaunchMsaView/components/useTranscriptSelection.test.tsx +66 -0
- package/src/LaunchMsaView/components/useTranscriptSelection.ts +41 -2
- package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
- package/src/LaunchMsaView/detectQueryRow.ts +22 -4
- package/src/LaunchMsaView/index.ts +32 -13
- package/src/LaunchMsaView/launchTarget.test.ts +88 -15
- package/src/LaunchMsaView/launchTarget.ts +49 -14
- package/src/LaunchMsaView/useQueryRowName.test.ts +54 -0
- package/src/LaunchMsaView/useQueryRowName.ts +22 -1
- package/src/LaunchMsaView/util.ts +8 -4
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +23 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +32 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +85 -27
- package/src/MsaViewPanel/components/LaunchProgress.tsx +38 -7
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +50 -2
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +10 -2
- package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +92 -142
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +13 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +55 -5
- package/src/MsaViewPanel/genomeToMSA.test.ts +251 -31
- package/src/MsaViewPanel/genomeToMSA.ts +35 -7
- package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
- package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
- package/src/MsaViewPanel/model.test.ts +196 -0
- package/src/MsaViewPanel/model.ts +204 -55
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
- package/src/MsaViewPanel/msaDataStore.ts +10 -0
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
- package/src/MsaViewPanel/processInit.test.ts +84 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +160 -0
- package/src/MsaViewPanel/runLaunch.test.ts +54 -0
- package/src/MsaViewPanel/runLaunch.ts +39 -0
- package/src/MsaViewPanel/storedData.test.ts +26 -0
- package/src/MsaViewPanel/structureConnection.ts +0 -26
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +14 -5
- package/src/MsaViewPanel/util.ts +75 -0
- package/src/utils/browserAlign.test.ts +102 -0
- package/src/utils/browserAlign.ts +352 -0
- package/src/utils/ebiBlast.ts +34 -0
- package/src/utils/homologSearch.ts +51 -0
- package/src/utils/msa.ts +19 -41
- package/src/utils/msaRows.ts +20 -16
- package/src/utils/phmmer.ts +57 -11
- package/src/utils/unirefHomologs.test.ts +149 -0
- package/src/utils/unirefHomologs.ts +323 -0
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/geneticCodes.d.ts +0 -15
- package/dist/LaunchMsaView/components/geneticCodes.js +0 -227
- package/dist/MsaViewPanel/structureConnection.test.js +0 -53
- package/src/LaunchMsaView/components/geneticCodes.ts +0 -298
- package/src/MsaViewPanel/structureConnection.test.ts +0 -62
- /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('
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
: '
|
|
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 {
|
|
1
|
+
import { revcom } from '@jbrowse/core/util';
|
|
2
2
|
import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides';
|
|
3
|
-
import { getGeneticCode, parseTranslTable } from '
|
|
4
|
-
// `@jbrowse/core/util/convertCodingSequenceToPeptides`
|
|
5
|
-
//
|
|
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') ?? []
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
43
|
+
open(feature);
|
|
25
44
|
}
|
|
26
45
|
})
|
|
27
46
|
.catch((e) => {
|