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,9 +4,10 @@ import type { JBrowsePluginMsaViewModel } from '../model';
|
|
|
4
4
|
* What a view shows while it is still building its alignment, and what it shows
|
|
5
5
|
* when that fails.
|
|
6
6
|
*
|
|
7
|
-
* Every launch that resolves something
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Every launch that resolves something states its request on the model --
|
|
8
|
+
* `blastParams`, `orthologParams`, `init` -- and one not yet marked
|
|
9
|
+
* `launchCompleted` IS "no alignment yet", as is a completed one whose stored
|
|
10
|
+
* alignment has since expired. The error a failed launch records is only
|
|
10
11
|
* readable here. This used to key on `blastParams` alone, which left an ortholog
|
|
11
12
|
* launch rendering an empty MSAView for the minutes its alignment takes and, on
|
|
12
13
|
* failure, forever: the error was set and nothing drew it.
|
|
@@ -18,27 +18,39 @@ const useStyles = makeStyles()({
|
|
|
18
18
|
* What a view shows while it is still building its alignment, and what it shows
|
|
19
19
|
* when that fails.
|
|
20
20
|
*
|
|
21
|
-
* Every launch that resolves something
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Every launch that resolves something states its request on the model --
|
|
22
|
+
* `blastParams`, `orthologParams`, `init` -- and one not yet marked
|
|
23
|
+
* `launchCompleted` IS "no alignment yet", as is a completed one whose stored
|
|
24
|
+
* alignment has since expired. The error a failed launch records is only
|
|
24
25
|
* readable here. This used to key on `blastParams` alone, which left an ortholog
|
|
25
26
|
* launch rendering an empty MSAView for the minutes its alignment takes and, on
|
|
26
27
|
* failure, forever: the error was set and nothing drew it.
|
|
27
28
|
*/
|
|
28
29
|
const LaunchProgress = observer(function LaunchProgress2({ model, }) {
|
|
29
|
-
const {
|
|
30
|
+
const { progress, rid, error } = model;
|
|
30
31
|
const { classes } = useStyles();
|
|
32
|
+
// the request stays on the model after it succeeds, so an expired
|
|
33
|
+
// alignment's panel is still named after the search that built it
|
|
34
|
+
const pending = !model.launchCompleted;
|
|
35
|
+
const { blastParams, orthologParams } = model;
|
|
31
36
|
const message = blastParams
|
|
32
|
-
?
|
|
37
|
+
? `Running EBI ${blastParams.searchProgram === 'phmmer' ? 'phmmer' : 'BLAST'}`
|
|
33
38
|
: orthologParams
|
|
34
|
-
?
|
|
39
|
+
? orthologParams.source === 'uniref'
|
|
40
|
+
? 'Building UniRef homolog alignment'
|
|
41
|
+
: 'Building ortholog alignment'
|
|
35
42
|
: 'Loading alignment';
|
|
36
43
|
return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
|
|
37
|
-
React.createElement(Typography, { variant: "h5" },
|
|
38
|
-
message,
|
|
39
|
-
" failed"),
|
|
44
|
+
React.createElement(Typography, { variant: "h5" }, pending ? `${message} failed` : 'Alignment no longer available'),
|
|
40
45
|
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
41
|
-
React.createElement(ErrorMessage, { error: error })
|
|
46
|
+
React.createElement(ErrorMessage, { error: error }),
|
|
47
|
+
React.createElement("div", { className: classes.progressRow },
|
|
48
|
+
React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
|
|
49
|
+
model.retryLaunch();
|
|
50
|
+
} }, "Retry"),
|
|
51
|
+
React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
|
|
52
|
+
model.cancelLaunch();
|
|
53
|
+
} }, "Dismiss")))) : (React.createElement(React.Fragment, null,
|
|
42
54
|
React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
|
|
43
55
|
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
44
56
|
React.createElement("div", { className: classes.progressRow },
|
|
@@ -14,8 +14,16 @@ const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
|
|
|
14
14
|
const { classes } = useStyles();
|
|
15
15
|
const { blastParams, orthologParams, init, loadingStoredData } = model;
|
|
16
16
|
// an unresolved launch request means there is no alignment to draw yet, so all
|
|
17
|
-
// three gate the same panel -- see LaunchProgress
|
|
18
|
-
|
|
17
|
+
// three gate the same panel -- see LaunchProgress. An indexed view keeps its
|
|
18
|
+
// init for the life of the view (it is how the block is refetched), so that
|
|
19
|
+
// one is only "launching" until the alignment arrives.
|
|
20
|
+
const request = blastParams ?? orthologParams;
|
|
21
|
+
const pending = !!request && !model.launchCompleted;
|
|
22
|
+
// a request that finished and then lost its alignment -- browser storage
|
|
23
|
+
// expires one after 7 days -- is a request again, and LaunchProgress draws it
|
|
24
|
+
// with the Retry that runs it
|
|
25
|
+
const expired = !!request && !!model.error && !model.dataInitialized;
|
|
26
|
+
const launching = pending || expired || (!!init && !model.dataInitialized);
|
|
19
27
|
return (React.createElement(ErrorBoundary, null,
|
|
20
28
|
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
|
|
21
29
|
React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
|
|
@@ -48,6 +48,16 @@ test('a failed ortholog launch shows why', () => {
|
|
|
48
48
|
});
|
|
49
49
|
expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy();
|
|
50
50
|
});
|
|
51
|
+
// an indexed view keeps its init -- it is how the block is refetched on the
|
|
52
|
+
// next session -- so the panel cannot read init alone as "still launching"
|
|
53
|
+
test('an indexed view with its alignment draws it, init and all', () => {
|
|
54
|
+
panel({
|
|
55
|
+
init: { msaIndexedLocation: { uri: 'msa.fa.gz' }, msaName: 'ENST1' },
|
|
56
|
+
dataInitialized: true,
|
|
57
|
+
progress: '',
|
|
58
|
+
});
|
|
59
|
+
expect(screen.getByText('the alignment')).toBeTruthy();
|
|
60
|
+
});
|
|
51
61
|
test('a failed init shows why', () => {
|
|
52
62
|
panel({
|
|
53
63
|
init: { msaName: 'ENST00000288602' },
|
|
@@ -58,7 +68,7 @@ test('a failed init shows why', () => {
|
|
|
58
68
|
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
|
|
59
69
|
});
|
|
60
70
|
// a launch runs for 10+ minutes, so leaving with no way out means watching it
|
|
61
|
-
test('a running launch offers a way out
|
|
71
|
+
test('a running launch offers a way out', () => {
|
|
62
72
|
const cancelLaunch = vi.fn();
|
|
63
73
|
panel({
|
|
64
74
|
blastParams: { proteinSequence: 'MKV' },
|
|
@@ -67,14 +77,49 @@ test('a running launch offers a way out, and a failed one does not', () => {
|
|
|
67
77
|
});
|
|
68
78
|
screen.getByRole('button', { name: 'Cancel' }).click();
|
|
69
79
|
expect(cancelLaunch).toHaveBeenCalled();
|
|
70
|
-
|
|
80
|
+
});
|
|
81
|
+
// the request outlives the failure -- it is what a reload resubmits -- so a
|
|
82
|
+
// failed launch that only drew the error was a dead end that re-ran the EBI job
|
|
83
|
+
// on every reload
|
|
84
|
+
test('a failed launch can be retried or dismissed', () => {
|
|
85
|
+
const cancelLaunch = vi.fn();
|
|
86
|
+
const retryLaunch = vi.fn();
|
|
71
87
|
panel({
|
|
72
88
|
blastParams: { proteinSequence: 'MKV' },
|
|
73
89
|
progress: '',
|
|
74
90
|
error: new Error('No hits found'),
|
|
75
91
|
cancelLaunch,
|
|
92
|
+
retryLaunch,
|
|
76
93
|
});
|
|
77
94
|
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
|
|
95
|
+
screen.getByRole('button', { name: 'Retry' }).click();
|
|
96
|
+
expect(retryLaunch).toHaveBeenCalled();
|
|
97
|
+
screen.getByRole('button', { name: 'Dismiss' }).click();
|
|
98
|
+
expect(cancelLaunch).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
// a stored alignment expires after 7 days, and the view used to say so and
|
|
101
|
+
// stop there -- the request that built it had been dropped on success, so the
|
|
102
|
+
// only way back was to find the gene again
|
|
103
|
+
test('an expired alignment offers to run its original search again', () => {
|
|
104
|
+
const retryLaunch = vi.fn();
|
|
105
|
+
panel({
|
|
106
|
+
progress: '',
|
|
107
|
+
dataInitialized: false,
|
|
108
|
+
error: new Error("This view's alignment is no longer in browser storage."),
|
|
109
|
+
blastParams: { blastDatabase: 'uniprotkb_swissprot' },
|
|
110
|
+
launchCompleted: true,
|
|
111
|
+
retryLaunch,
|
|
112
|
+
});
|
|
113
|
+
expect(screen.queryByText('the alignment')).toBeNull();
|
|
114
|
+
expect(screen.getByText('Alignment no longer available')).toBeTruthy();
|
|
115
|
+
screen.getByRole('button', { name: 'Retry' }).click();
|
|
116
|
+
expect(retryLaunch).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
// with nothing to retry from -- a pasted alignment, say -- the panel stays out
|
|
119
|
+
// of the way and react-msaview's own import form is what the user gets
|
|
120
|
+
test('an expired alignment with no request behind it draws the view', () => {
|
|
121
|
+
panel({ progress: '', dataInitialized: false, error: new Error('gone') });
|
|
122
|
+
expect(screen.getByText('the alignment')).toBeTruthy();
|
|
78
123
|
});
|
|
79
124
|
test('a running job links out to it', () => {
|
|
80
125
|
panel({
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
2
|
import type { LaunchScope } from './runLaunch';
|
|
3
|
+
/**
|
|
4
|
+
* A similarity search, then an alignment of what it found. The program is a
|
|
5
|
+
* backend behind one interface (utils/homologSearch.ts); what differs between
|
|
6
|
+
* them is settled by whether the result came back aligned. A program that
|
|
7
|
+
* aligns as it searches (phmmer) hands over the alignment and the tree is
|
|
8
|
+
* built from it in the browser; one that does not (blastp) hands over bare
|
|
9
|
+
* hits and the chosen aligner runs on them.
|
|
10
|
+
*/
|
|
3
11
|
export declare function doLaunchBlast({ self, scope, }: {
|
|
4
12
|
self: JBrowsePluginMsaViewModel;
|
|
5
13
|
scope: LaunchScope;
|
|
@@ -1,114 +1,99 @@
|
|
|
1
|
-
import { makeId, strip } from '../LaunchMsaView/components/util';
|
|
2
1
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
3
2
|
import { saveBlastResult } from '../utils/blastCache';
|
|
4
|
-
import {
|
|
5
|
-
import { launchMSA
|
|
6
|
-
import {
|
|
7
|
-
import { queryPhmmer } from '../utils/phmmer';
|
|
3
|
+
import { searchBackends } from '../utils/homologSearch';
|
|
4
|
+
import { launchMSA } from '../utils/msa';
|
|
5
|
+
import { buildSearchMsa } from '../utils/msaRows';
|
|
8
6
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const { selectedTranscript } = params;
|
|
14
|
-
const cleanedSeq = cleanProteinSequence(params.proteinSequence);
|
|
15
|
-
const { onProgress, onRid, signal } = scope;
|
|
16
|
-
const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
|
|
17
|
-
? await runPhmmer({
|
|
18
|
-
query: cleanedSeq,
|
|
19
|
-
database: params.blastDatabase,
|
|
20
|
-
onProgress,
|
|
21
|
-
onRid,
|
|
22
|
-
signal,
|
|
23
|
-
})
|
|
24
|
-
: await runBlast({
|
|
25
|
-
query: cleanedSeq,
|
|
26
|
-
blastDatabase: params.blastDatabase,
|
|
27
|
-
msaAlgorithm: params.msaAlgorithm,
|
|
28
|
-
onProgress,
|
|
29
|
-
onRid,
|
|
30
|
-
signal,
|
|
31
|
-
});
|
|
32
|
-
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
33
|
-
await saveBlastResult({
|
|
34
|
-
proteinSequence: cleanedSeq,
|
|
35
|
-
blastDatabase: params.blastDatabase,
|
|
36
|
-
msaAlgorithm: params.msaAlgorithm,
|
|
37
|
-
searchProgram: params.searchProgram,
|
|
38
|
-
msa,
|
|
39
|
-
tree,
|
|
40
|
-
treeMetadata: treeMetadataJson,
|
|
41
|
-
rid,
|
|
42
|
-
geneId: selectedTranscript?.get('parentId'),
|
|
43
|
-
transcriptId: selectedTranscript?.id(),
|
|
44
|
-
transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
|
|
45
|
-
geneName: selectedTranscript?.get('gene_name') ??
|
|
46
|
-
selectedTranscript?.get('parentId'),
|
|
47
|
-
});
|
|
48
|
-
return { msa, tree, treeMetadata: treeMetadataJson };
|
|
7
|
+
import { resolveUniProtEntry } from '../utils/unirefHomologs';
|
|
8
|
+
import { transcriptFields, transcriptName } from './util';
|
|
9
|
+
function asString(val) {
|
|
10
|
+
return typeof val === 'string' ? val : undefined;
|
|
49
11
|
}
|
|
50
12
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
13
|
+
* The query sequence, and what its row is called. The dialog hands over the
|
|
14
|
+
* translated transcript and the row stays `QUERY`; a spec naming a UniProt
|
|
15
|
+
* accession has the sequence fetched and the row named after the entry
|
|
16
|
+
* (`P53_HUMAN_query`), since a search of swissprot returns the entry itself
|
|
17
|
+
* as a hit and two rows called the same thing collapse into one.
|
|
54
18
|
*/
|
|
55
|
-
async function
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
signal,
|
|
62
|
-
});
|
|
63
|
-
onProgress('Fetching species taxonomy info...');
|
|
64
|
-
const taxonomyInfo = await fetchTaxonomyInfo(hits
|
|
65
|
-
.map(h => h.description[0]?.taxid)
|
|
66
|
-
.filter((t) => t !== undefined));
|
|
67
|
-
const treeMetadata = {};
|
|
68
|
-
const sequences = hits.map(h => {
|
|
69
|
-
const desc = h.description[0] ?? {
|
|
70
|
-
accession: 'unknown',
|
|
71
|
-
id: 'unknown',
|
|
72
|
-
sciname: 'unknown',
|
|
19
|
+
async function resolveQuery(self, scope) {
|
|
20
|
+
const params = self.blastParams;
|
|
21
|
+
if (params.proteinSequence) {
|
|
22
|
+
return {
|
|
23
|
+
sequence: cleanProteinSequence(params.proteinSequence),
|
|
24
|
+
name: self.querySeqName,
|
|
73
25
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
26
|
+
}
|
|
27
|
+
if (params.accession) {
|
|
28
|
+
scope.onProgress(`Fetching ${params.accession} from UniProt...`);
|
|
29
|
+
const entry = await resolveUniProtEntry([params.accession], 0, scope.signal);
|
|
30
|
+
if (!entry) {
|
|
31
|
+
throw new Error(`UniProt has no entry ${params.accession}`);
|
|
32
|
+
}
|
|
33
|
+
const name = `${entry.id}_query`;
|
|
34
|
+
scope.act(() => {
|
|
35
|
+
self.setQuerySeqName(name);
|
|
36
|
+
});
|
|
37
|
+
return { sequence: entry.sequence, name };
|
|
38
|
+
}
|
|
39
|
+
throw new Error('No query: a search needs a proteinSequence, a UniProt accession, or a connectedTranscript to translate');
|
|
85
40
|
}
|
|
86
41
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
42
|
+
* A similarity search, then an alignment of what it found. The program is a
|
|
43
|
+
* backend behind one interface (utils/homologSearch.ts); what differs between
|
|
44
|
+
* them is settled by whether the result came back aligned. A program that
|
|
45
|
+
* aligns as it searches (phmmer) hands over the alignment and the tree is
|
|
46
|
+
* built from it in the browser; one that does not (blastp) hands over bare
|
|
47
|
+
* hits and the chosen aligner runs on them.
|
|
92
48
|
*/
|
|
93
|
-
async function
|
|
94
|
-
const
|
|
49
|
+
export async function doLaunchBlast({ self, scope, }) {
|
|
50
|
+
const params = self.blastParams;
|
|
51
|
+
const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
|
|
52
|
+
const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
|
|
53
|
+
const { onProgress, onRid, signal } = scope;
|
|
54
|
+
const { hits, queryRow, rid } = await searchBackends[searchProgram]({
|
|
95
55
|
query,
|
|
96
|
-
database,
|
|
56
|
+
database: params.blastDatabase,
|
|
57
|
+
maxHits,
|
|
97
58
|
onProgress,
|
|
98
59
|
onRid,
|
|
99
60
|
signal,
|
|
100
61
|
});
|
|
62
|
+
if (hits.length === 0) {
|
|
63
|
+
throw new Error('No hits found');
|
|
64
|
+
}
|
|
101
65
|
onProgress('Fetching species taxonomy info...');
|
|
102
|
-
const taxonomyInfo = await fetchTaxonomyInfo(
|
|
103
|
-
const { msa, treeMetadata } =
|
|
104
|
-
|
|
66
|
+
const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined));
|
|
67
|
+
const { msa: fasta, treeMetadata } = buildSearchMsa({
|
|
68
|
+
hits,
|
|
69
|
+
query,
|
|
105
70
|
queryRow,
|
|
106
71
|
taxonomyInfo,
|
|
72
|
+
querySeqName,
|
|
107
73
|
});
|
|
108
|
-
|
|
74
|
+
const { msa, tree } = queryRow
|
|
75
|
+
? { msa: fasta, tree: '' }
|
|
76
|
+
: await launchMSA({
|
|
77
|
+
algorithm: params.msaAlgorithm ?? 'browser',
|
|
78
|
+
sequence: fasta,
|
|
79
|
+
onProgress,
|
|
80
|
+
signal,
|
|
81
|
+
});
|
|
82
|
+
const transcript = transcriptFields(selectedTranscript);
|
|
83
|
+
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
84
|
+
await saveBlastResult({
|
|
85
|
+
proteinSequence: query,
|
|
86
|
+
blastDatabase: params.blastDatabase,
|
|
87
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
88
|
+
searchProgram: params.searchProgram,
|
|
109
89
|
msa,
|
|
110
|
-
tree
|
|
111
|
-
treeMetadata,
|
|
112
|
-
rid,
|
|
113
|
-
|
|
90
|
+
tree,
|
|
91
|
+
treeMetadata: treeMetadataJson,
|
|
92
|
+
rid: rid ?? '',
|
|
93
|
+
geneId: asString(transcript.parentId),
|
|
94
|
+
transcriptId: asString(transcript.uniqueId),
|
|
95
|
+
transcriptName: transcriptName(selectedTranscript),
|
|
96
|
+
geneName: asString(transcript.gene_name) ?? asString(transcript.parentId),
|
|
97
|
+
});
|
|
98
|
+
return { msa, tree, treeMetadata: treeMetadataJson };
|
|
114
99
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { beforeEach, expect, test, vi } from 'vitest';
|
|
2
|
+
import { saveBlastResult } from '../utils/blastCache';
|
|
3
|
+
import { searchBackends } from '../utils/homologSearch';
|
|
4
|
+
import { launchMSA } from '../utils/msa';
|
|
5
|
+
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
|
+
import { resolveUniProtEntry } from '../utils/unirefHomologs';
|
|
7
|
+
import { doLaunchBlast } from './doLaunchBlast';
|
|
8
|
+
// Every network call is mocked: what is under test is how the launch turns a
|
|
9
|
+
// request into a search and the search's answer into rows -- which backend it
|
|
10
|
+
// asks, where the query comes from, and whether an aligner runs.
|
|
11
|
+
vi.mock('../utils/homologSearch', () => ({
|
|
12
|
+
searchBackends: { blastp: vi.fn(), phmmer: vi.fn() },
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
|
|
15
|
+
vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
|
|
16
|
+
vi.mock('../utils/blastCache', () => ({ saveBlastResult: vi.fn() }));
|
|
17
|
+
vi.mock('../utils/unirefHomologs', () => ({ resolveUniProtEntry: vi.fn() }));
|
|
18
|
+
const blastp = vi.mocked(searchBackends.blastp);
|
|
19
|
+
const phmmer = vi.mocked(searchBackends.phmmer);
|
|
20
|
+
const mockLaunchMSA = vi.mocked(launchMSA);
|
|
21
|
+
const setQuerySeqName = vi.fn();
|
|
22
|
+
const HIT = { accession: 'P1', id: 'P1_MOUSE', sciname: 'Mus musculus' };
|
|
23
|
+
function makeModel(blastParams) {
|
|
24
|
+
return {
|
|
25
|
+
blastParams,
|
|
26
|
+
querySeqName: 'QUERY',
|
|
27
|
+
setQuerySeqName,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function launch(self) {
|
|
31
|
+
return doLaunchBlast({
|
|
32
|
+
self,
|
|
33
|
+
scope: {
|
|
34
|
+
signal: new AbortController().signal,
|
|
35
|
+
act: fn => {
|
|
36
|
+
fn();
|
|
37
|
+
},
|
|
38
|
+
onProgress: () => { },
|
|
39
|
+
onRid: () => { },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
vi.mocked(fetchTaxonomyInfo).mockResolvedValue(new Map());
|
|
46
|
+
vi.mocked(saveBlastResult).mockResolvedValue(undefined);
|
|
47
|
+
});
|
|
48
|
+
test('bare hits go to the chosen aligner, with the query first', async () => {
|
|
49
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
50
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
|
|
51
|
+
const result = await launch(makeModel({
|
|
52
|
+
searchProgram: 'blastp',
|
|
53
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
54
|
+
msaAlgorithm: 'clustalo',
|
|
55
|
+
maxHits: 20,
|
|
56
|
+
proteinSequence: 'MKWVTF*',
|
|
57
|
+
}));
|
|
58
|
+
expect(blastp).toHaveBeenCalledWith(expect.objectContaining({
|
|
59
|
+
query: 'MKWVTF',
|
|
60
|
+
database: 'uniprotkb_swissprot',
|
|
61
|
+
maxHits: 20,
|
|
62
|
+
}));
|
|
63
|
+
expect(mockLaunchMSA).toHaveBeenCalledWith(expect.objectContaining({
|
|
64
|
+
algorithm: 'clustalo',
|
|
65
|
+
sequence: '>QUERY\nMKWVTF\n>P1-Mus_musculus\nMKWV',
|
|
66
|
+
}));
|
|
67
|
+
expect(result.msa).toBe('aligned');
|
|
68
|
+
expect(result.tree).toBe('tree');
|
|
69
|
+
});
|
|
70
|
+
test('an aligned result skips the aligner and leaves the tree to the browser', async () => {
|
|
71
|
+
phmmer.mockResolvedValue({
|
|
72
|
+
rid: 'job',
|
|
73
|
+
queryRow: 'MKWV-TF',
|
|
74
|
+
hits: [{ ...HIT, sequence: 'MKWVSTF' }],
|
|
75
|
+
});
|
|
76
|
+
const result = await launch(makeModel({
|
|
77
|
+
searchProgram: 'phmmer',
|
|
78
|
+
blastDatabase: 'rp15',
|
|
79
|
+
proteinSequence: 'MKWVTF',
|
|
80
|
+
}));
|
|
81
|
+
expect(mockLaunchMSA).not.toHaveBeenCalled();
|
|
82
|
+
expect(result.msa).toBe('>QUERY\nMKWV-TF\n>P1-Mus_musculus\nMKWVSTF');
|
|
83
|
+
expect(result.tree).toBe('');
|
|
84
|
+
});
|
|
85
|
+
test('a UniProt accession supplies the query and names its row', async () => {
|
|
86
|
+
vi.mocked(resolveUniProtEntry).mockResolvedValue({
|
|
87
|
+
accession: 'P04637',
|
|
88
|
+
id: 'P53_HUMAN',
|
|
89
|
+
reviewed: true,
|
|
90
|
+
taxId: 9606,
|
|
91
|
+
scientificName: 'Homo sapiens',
|
|
92
|
+
sequence: 'MEEPQ',
|
|
93
|
+
});
|
|
94
|
+
phmmer.mockResolvedValue({
|
|
95
|
+
queryRow: 'MEEPQ',
|
|
96
|
+
hits: [{ ...HIT, sequence: 'MEEPQ' }],
|
|
97
|
+
});
|
|
98
|
+
const result = await launch(makeModel({
|
|
99
|
+
searchProgram: 'phmmer',
|
|
100
|
+
blastDatabase: 'swissprot',
|
|
101
|
+
accession: 'P04637',
|
|
102
|
+
}));
|
|
103
|
+
expect(resolveUniProtEntry).toHaveBeenCalledWith(['P04637'], 0, expect.any(AbortSignal));
|
|
104
|
+
expect(setQuerySeqName).toHaveBeenCalledWith('P53_HUMAN_query');
|
|
105
|
+
expect(result.msa.split('\n')[0]).toBe('>P53_HUMAN_query');
|
|
106
|
+
});
|
|
107
|
+
test('a request with no query at all is refused before any search runs', async () => {
|
|
108
|
+
await expect(launch(makeModel({ searchProgram: 'phmmer', blastDatabase: 'swissprot' }))).rejects.toThrow(/connectedTranscript/);
|
|
109
|
+
expect(phmmer).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
// blastParams is a frozen snapshot property, so the Feature the dialog put in
|
|
112
|
+
// it comes back from a session reload as the plain JSON it serialized to. The
|
|
113
|
+
// launch used to call .get() on that, minutes after the EBI job the reload
|
|
114
|
+
// resubmitted had come back, and threw where nothing was catching.
|
|
115
|
+
test('a transcript restored from a session snapshot still labels the cache row', async () => {
|
|
116
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
117
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
|
|
118
|
+
await launch(makeModel({
|
|
119
|
+
searchProgram: 'blastp',
|
|
120
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
121
|
+
msaAlgorithm: 'clustalo',
|
|
122
|
+
proteinSequence: 'MKWV',
|
|
123
|
+
selectedTranscript: {
|
|
124
|
+
uniqueId: 'NM_000546.6',
|
|
125
|
+
name: 'TP53-201',
|
|
126
|
+
parentId: 'TP53',
|
|
127
|
+
gene_name: 'TP53',
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
|
|
131
|
+
geneId: 'TP53',
|
|
132
|
+
transcriptId: 'NM_000546.6',
|
|
133
|
+
transcriptName: 'TP53-201',
|
|
134
|
+
geneName: 'TP53',
|
|
135
|
+
}));
|
|
136
|
+
});
|
|
137
|
+
test('a live Feature handed over in the same session labels it the same way', async () => {
|
|
138
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
139
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
|
|
140
|
+
const json = {
|
|
141
|
+
uniqueId: 'NM_000546.6',
|
|
142
|
+
name: 'TP53-201',
|
|
143
|
+
parentId: 'TP53',
|
|
144
|
+
};
|
|
145
|
+
await launch(makeModel({
|
|
146
|
+
searchProgram: 'blastp',
|
|
147
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
148
|
+
msaAlgorithm: 'clustalo',
|
|
149
|
+
proteinSequence: 'MKWV',
|
|
150
|
+
selectedTranscript: { toJSON: () => json },
|
|
151
|
+
}));
|
|
152
|
+
expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
|
|
153
|
+
geneId: 'TP53',
|
|
154
|
+
transcriptId: 'NM_000546.6',
|
|
155
|
+
transcriptName: 'TP53-201',
|
|
156
|
+
geneName: 'TP53',
|
|
157
|
+
}));
|
|
158
|
+
});
|
|
@@ -3,6 +3,8 @@ import { launchMSA } from '../utils/msa';
|
|
|
3
3
|
import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
4
4
|
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
5
5
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
|
+
import { fetchUnirefHomologs } from '../utils/unirefHomologs';
|
|
7
|
+
import { transcriptName } from './util';
|
|
6
8
|
/**
|
|
7
9
|
* The no-search-job alternative to doLaunchBlast.
|
|
8
10
|
*
|
|
@@ -21,7 +23,7 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
|
21
23
|
* excluded from the ortholog set rather than appearing twice.
|
|
22
24
|
*/
|
|
23
25
|
export async function doLaunchOrthologs({ self, scope, }) {
|
|
24
|
-
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
|
|
26
|
+
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', identity, referenceProteomesOnly, } = self.orthologParams;
|
|
25
27
|
const { onProgress, act, signal } = scope;
|
|
26
28
|
const request = {
|
|
27
29
|
taxId,
|
|
@@ -34,7 +36,14 @@ export async function doLaunchOrthologs({ self, scope, }) {
|
|
|
34
36
|
};
|
|
35
37
|
const { geneId, representative, rows } = source === 'panther'
|
|
36
38
|
? await findPantherOrthologs(request)
|
|
37
|
-
:
|
|
39
|
+
: source === 'uniref'
|
|
40
|
+
? await findUnirefHomologs({
|
|
41
|
+
...request,
|
|
42
|
+
identity,
|
|
43
|
+
referenceProteomesOnly,
|
|
44
|
+
signal,
|
|
45
|
+
})
|
|
46
|
+
: await findNcbiOrthologs(request);
|
|
38
47
|
// The query row. The dialog always supplies it — it is the user's OWN
|
|
39
48
|
// selected transcript, which is what makes `connectedFeature` map genome
|
|
40
49
|
// coordinates through this row. A launch that has no transcript to translate
|
|
@@ -46,7 +55,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
|
|
|
46
55
|
? cleanProteinSequence(proteinSequence)
|
|
47
56
|
: representative?.sequence;
|
|
48
57
|
if (!cleanedSeq) {
|
|
49
|
-
throw new Error(`No query protein: none was supplied and ${source
|
|
58
|
+
throw new Error(`No query protein: none was supplied and ${sourceNames[source]} returned no representative protein for gene ${geneId}.`);
|
|
50
59
|
}
|
|
51
60
|
// The query row is named for its species like every other row, with a suffix
|
|
52
61
|
// marking it as the one the genome view is linked to. A bare `QUERY` among
|
|
@@ -103,6 +112,33 @@ async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest })
|
|
|
103
112
|
});
|
|
104
113
|
return { geneId: resolved.geneId, representative, rows };
|
|
105
114
|
}
|
|
115
|
+
const sourceNames = {
|
|
116
|
+
ncbi: 'NCBI',
|
|
117
|
+
panther: 'PANTHER',
|
|
118
|
+
uniref: 'UniProt',
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The query's own UniProt entry stands in for a representative protein: the
|
|
122
|
+
* entry the cluster was looked up from, whose sequence a spec-launched row
|
|
123
|
+
* takes and whose accession earns the CDD overlay when the two match.
|
|
124
|
+
*/
|
|
125
|
+
async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, signal, ...rest }) {
|
|
126
|
+
const found = await fetchUnirefHomologs({
|
|
127
|
+
candidates: geneCandidates,
|
|
128
|
+
identity,
|
|
129
|
+
referenceProteomesOnly,
|
|
130
|
+
signal,
|
|
131
|
+
...rest,
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
geneId: found.query.id,
|
|
135
|
+
representative: {
|
|
136
|
+
accession: found.query.accession,
|
|
137
|
+
sequence: found.query.sequence,
|
|
138
|
+
},
|
|
139
|
+
rows: found.rows,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
106
142
|
/**
|
|
107
143
|
* One `matchortho` call resolves the gene, names its own UniProt entry and
|
|
108
144
|
* lists an ortholog per genome, so the representative protein needs no second
|
|
@@ -162,9 +198,8 @@ async function fetchRepresentativeQueryProtein(geneId) {
|
|
|
162
198
|
* protein as its query row passes that test by construction.
|
|
163
199
|
*/
|
|
164
200
|
function buildQueryMetadata(self, geneId, proteinSequence, representative) {
|
|
165
|
-
const transcript = self.orthologParams?.selectedTranscript;
|
|
166
201
|
const metadata = { 'Gene ID': geneId };
|
|
167
|
-
const name =
|
|
202
|
+
const name = transcriptName(self.orthologParams?.selectedTranscript);
|
|
168
203
|
if (name) {
|
|
169
204
|
metadata.Transcript = name;
|
|
170
205
|
}
|
|
@@ -203,6 +203,16 @@ describe('the Accession that drives the domain overlay', () => {
|
|
|
203
203
|
Accession: REPRESENTATIVE.accession,
|
|
204
204
|
});
|
|
205
205
|
});
|
|
206
|
+
// orthologParams is frozen, so a session reload hands the transcript back as
|
|
207
|
+
// the plain JSON it serialized to; .get() on that threw
|
|
208
|
+
test('names the transcript from a snapshot-restored transcript', async () => {
|
|
209
|
+
const result = await launch({
|
|
210
|
+
self: makeModel(params({
|
|
211
|
+
selectedTranscript: { uniqueId: 'NM_1', name: 'TP53-201' },
|
|
212
|
+
})),
|
|
213
|
+
});
|
|
214
|
+
expect(queryMetadata(result).Transcript).toBe('TP53-201');
|
|
215
|
+
});
|
|
206
216
|
test('is withheld from a non-representative isoform', async () => {
|
|
207
217
|
const result = await launch({
|
|
208
218
|
self: makeModel(params({ proteinSequence: 'MDIFFERENTISOFORM' })),
|