jbrowse-plugin-msaview 3.1.0 → 3.3.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/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +6 -4
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +4 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +8 -3
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +4 -2
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +1 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +4 -2
- package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +4 -1
- package/dist/LaunchMsaView/components/SubmitCancelActions.js +34 -10
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +55 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +2 -2
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +105 -0
- package/dist/MsaViewPanel/model.d.ts +9 -0
- package/dist/MsaViewPanel/model.js +6 -0
- package/dist/index.js +4 -1
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +30 -30
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/launchMsaView.d.ts +19 -0
- package/dist/utils/launchMsaView.js +13 -0
- package/dist/utils/pantherOrthologs.d.ts +79 -0
- package/dist/utils/pantherOrthologs.js +262 -0
- package/dist/utils/workspaces.d.ts +34 -0
- package/dist/utils/workspaces.js +100 -0
- package/dist/utils/workspaces.test.d.ts +1 -0
- package/dist/utils/workspaces.test.js +100 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +7 -4
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +5 -2
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +20 -4
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
- package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +5 -2
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +1 -0
- package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +5 -2
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +95 -0
- package/src/LaunchMsaView/components/SubmitCancelActions.tsx +80 -22
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -2
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +117 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +99 -37
- package/src/MsaViewPanel/model.ts +10 -0
- package/src/index.ts +4 -1
- package/src/utils/launchMsaView.ts +30 -0
- package/src/utils/pantherOrthologs.ts +399 -0
- package/src/utils/workspaces.test.ts +132 -0
- package/src/utils/workspaces.ts +146 -0
- package/src/version.ts +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
+
import { launchMsaView } from '../../../utils/launchMsaView'
|
|
4
|
+
import { readLaunchPlacement } from '../../../utils/workspaces'
|
|
5
|
+
|
|
3
6
|
import type { BlastParams } from '../../../MsaViewPanel/model'
|
|
4
7
|
import type { CachedBlastResult } from '../../../utils/blastCache'
|
|
5
8
|
import type { Feature } from '@jbrowse/core/util'
|
|
@@ -16,8 +19,8 @@ export function blastLaunchView({
|
|
|
16
19
|
feature: Feature
|
|
17
20
|
blastParams: BlastParams
|
|
18
21
|
}) {
|
|
19
|
-
getSession(view)
|
|
20
|
-
|
|
22
|
+
launchMsaView(getSession(view), {
|
|
23
|
+
placement: readLaunchPlacement(),
|
|
21
24
|
displayName: newViewTitle,
|
|
22
25
|
connectedViewId: view.id,
|
|
23
26
|
connectedFeature: feature.toJSON(),
|
|
@@ -39,8 +42,8 @@ export function blastLaunchViewFromCache({
|
|
|
39
42
|
cached: CachedBlastResult
|
|
40
43
|
connectedFeature?: ReturnType<Feature['toJSON']>
|
|
41
44
|
}) {
|
|
42
|
-
getSession(view)
|
|
43
|
-
|
|
45
|
+
launchMsaView(getSession(view), {
|
|
46
|
+
placement: readLaunchPlacement(),
|
|
44
47
|
displayName: newViewTitle,
|
|
45
48
|
connectedViewId: view.id,
|
|
46
49
|
connectedFeature,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
+
import { launchMsaView } from '../../../utils/launchMsaView'
|
|
4
|
+
import { readLaunchPlacement } from '../../../utils/workspaces'
|
|
5
|
+
|
|
3
6
|
import type { Feature, FileLocation } from '@jbrowse/core/util'
|
|
4
7
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
5
8
|
|
|
@@ -23,8 +26,8 @@ export function launchView({
|
|
|
23
26
|
tree?: string
|
|
24
27
|
}
|
|
25
28
|
}) {
|
|
26
|
-
getSession(view)
|
|
27
|
-
|
|
29
|
+
launchMsaView(getSession(view), {
|
|
30
|
+
placement: readLaunchPlacement(),
|
|
28
31
|
displayName: newViewTitle,
|
|
29
32
|
connectedViewId: view.id,
|
|
30
33
|
connectedFeature: feature.toJSON(),
|
|
@@ -4,10 +4,14 @@ import { Typography } from '@mui/material'
|
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
import { makeStyles } from 'tss-react/mui'
|
|
6
6
|
|
|
7
|
+
import OrthologSourceSelect, {
|
|
8
|
+
ORTHOLOG_SOURCE_STORAGE_KEY,
|
|
9
|
+
} from './OrthologSourceSelect'
|
|
7
10
|
import QuerySpeciesSelect from './QuerySpeciesSelect'
|
|
8
11
|
import { orthologLaunchView } from './orthologLaunchView'
|
|
9
12
|
import TextField2 from '../../../components/TextField2'
|
|
10
13
|
import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs'
|
|
14
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage'
|
|
11
15
|
import {
|
|
12
16
|
getGeneDisplayName,
|
|
13
17
|
getGeneIdentifiers,
|
|
@@ -20,6 +24,7 @@ import SubmitCancelActions from '../SubmitCancelActions'
|
|
|
20
24
|
import TranscriptSelector from '../TranscriptSelector'
|
|
21
25
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
22
26
|
|
|
27
|
+
import type { OrthologSource } from '../../../MsaViewPanel/model'
|
|
23
28
|
import type { MsaAlgorithm } from '../BlastQuery/consts'
|
|
24
29
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
25
30
|
|
|
@@ -42,6 +47,10 @@ const OrthologPanel = observer(function ({
|
|
|
42
47
|
const view = getLinearGenomeView(model)
|
|
43
48
|
const [launchViewError, setLaunchViewError] = useState<unknown>()
|
|
44
49
|
const [taxId, setTaxId] = useState(9606)
|
|
50
|
+
const [source, setSource] = useLocalStorage<OrthologSource>(
|
|
51
|
+
ORTHOLOG_SOURCE_STORAGE_KEY,
|
|
52
|
+
'ncbi',
|
|
53
|
+
)
|
|
45
54
|
const [msaAlgorithm, setMsaAlgorithm] = useState<MsaAlgorithm>('clustalo')
|
|
46
55
|
const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies))
|
|
47
56
|
|
|
@@ -65,11 +74,17 @@ const OrthologPanel = observer(function ({
|
|
|
65
74
|
instant, and it is now the aligner that costs the wait, about half a
|
|
66
75
|
second per row. */}
|
|
67
76
|
<Typography variant="body2">
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
Precomputed orthologs, one gene per species, looked up rather than
|
|
78
|
+
searched for. No BLAST job to queue.
|
|
70
79
|
</Typography>
|
|
71
80
|
|
|
72
81
|
<div>
|
|
82
|
+
<OrthologSourceSelect
|
|
83
|
+
className={classes.selectField}
|
|
84
|
+
value={source}
|
|
85
|
+
onChange={setSource}
|
|
86
|
+
/>
|
|
87
|
+
|
|
73
88
|
<QuerySpeciesSelect
|
|
74
89
|
className={classes.selectField}
|
|
75
90
|
value={taxId}
|
|
@@ -93,14 +108,14 @@ const OrthologPanel = observer(function ({
|
|
|
93
108
|
setMaxSpecies(event.target.value)
|
|
94
109
|
}}
|
|
95
110
|
error={!rowCountValid}
|
|
96
|
-
helperText=
|
|
111
|
+
helperText={`the closest N species ${source === 'panther' ? 'PANTHER' : 'NCBI'} has`}
|
|
97
112
|
/>
|
|
98
113
|
</div>
|
|
99
114
|
|
|
100
115
|
<TranscriptSelector feature={feature} {...transcriptSelection} />
|
|
101
|
-
|
|
102
116
|
</LaunchPanelContent>
|
|
103
117
|
<SubmitCancelActions
|
|
118
|
+
model={model}
|
|
104
119
|
submitDisabled={!proteinSequence || !rowCountValid}
|
|
105
120
|
onSubmit={() => {
|
|
106
121
|
try {
|
|
@@ -112,6 +127,7 @@ const OrthologPanel = observer(function ({
|
|
|
112
127
|
newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
|
|
113
128
|
orthologParams: {
|
|
114
129
|
taxId,
|
|
130
|
+
source,
|
|
115
131
|
maxSpecies: rowCount,
|
|
116
132
|
geneCandidates,
|
|
117
133
|
msaAlgorithm,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { MenuItem } from '@mui/material'
|
|
4
|
+
|
|
5
|
+
import TextField2 from '../../../components/TextField2'
|
|
6
|
+
|
|
7
|
+
import type { OrthologSource } from '../../../MsaViewPanel/model'
|
|
8
|
+
|
|
9
|
+
export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source'
|
|
10
|
+
|
|
11
|
+
export const orthologSourceLabels: Record<OrthologSource, string> = {
|
|
12
|
+
ncbi: 'NCBI orthologs',
|
|
13
|
+
panther: 'PANTHER',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Which species a source can answer for, in the words a reader picking one
|
|
17
|
+
// needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
|
|
18
|
+
// from human to yeast and Arabidopsis.
|
|
19
|
+
const hints: Record<OrthologSource, string> = {
|
|
20
|
+
ncbi: 'vertebrates and insects',
|
|
21
|
+
panther: 'also yeast, worm, fly and plants',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function OrthologSourceSelect({
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
className,
|
|
28
|
+
}: {
|
|
29
|
+
value: OrthologSource
|
|
30
|
+
onChange: (val: OrthologSource) => void
|
|
31
|
+
className?: string
|
|
32
|
+
}) {
|
|
33
|
+
return (
|
|
34
|
+
<TextField2
|
|
35
|
+
variant="outlined"
|
|
36
|
+
label="Source"
|
|
37
|
+
className={className}
|
|
38
|
+
select
|
|
39
|
+
value={value}
|
|
40
|
+
helperText={hints[value]}
|
|
41
|
+
onChange={event => {
|
|
42
|
+
onChange(event.target.value as OrthologSource)
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{(Object.keys(orthologSourceLabels) as OrthologSource[]).map(val => (
|
|
46
|
+
<MenuItem value={val} key={val}>
|
|
47
|
+
{orthologSourceLabels[val]}
|
|
48
|
+
</MenuItem>
|
|
49
|
+
))}
|
|
50
|
+
</TextField2>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
+
import { launchMsaView } from '../../../utils/launchMsaView'
|
|
4
|
+
import { readLaunchPlacement } from '../../../utils/workspaces'
|
|
5
|
+
|
|
3
6
|
import type { OrthologParams } from '../../../MsaViewPanel/model'
|
|
4
7
|
import type { Feature } from '@jbrowse/core/util'
|
|
5
8
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
@@ -15,8 +18,8 @@ export function orthologLaunchView({
|
|
|
15
18
|
feature: Feature
|
|
16
19
|
orthologParams: OrthologParams
|
|
17
20
|
}) {
|
|
18
|
-
getSession(view)
|
|
19
|
-
|
|
21
|
+
launchMsaView(getSession(view), {
|
|
22
|
+
placement: readLaunchPlacement(),
|
|
20
23
|
displayName: newViewTitle,
|
|
21
24
|
connectedViewId: view.id,
|
|
22
25
|
connectedFeature: feature.toJSON(),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
+
import { launchMsaView } from '../../../utils/launchMsaView'
|
|
4
|
+
import { readLaunchPlacement } from '../../../utils/workspaces'
|
|
5
|
+
|
|
3
6
|
import type { Feature } from '@jbrowse/core/util'
|
|
4
7
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
5
8
|
|
|
@@ -16,8 +19,8 @@ export function preCalculatedLaunchView({
|
|
|
16
19
|
feature: Feature
|
|
17
20
|
querySeqName: string
|
|
18
21
|
}) {
|
|
19
|
-
getSession(view)
|
|
20
|
-
|
|
22
|
+
launchMsaView(getSession(view), {
|
|
23
|
+
placement: readLaunchPlacement(),
|
|
21
24
|
displayName: newViewTitle,
|
|
22
25
|
treeAreaWidth: 200,
|
|
23
26
|
querySeqName,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
5
|
+
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import SubmitCancelActions from './SubmitCancelActions'
|
|
8
|
+
import { LAUNCH_PLACEMENT_KEY } from '../../utils/workspaces'
|
|
9
|
+
|
|
10
|
+
import type { AbstractTrackModel } from '@jbrowse/core/util'
|
|
11
|
+
|
|
12
|
+
// getSession walks the MST tree, and this component only wants the two actions
|
|
13
|
+
// off the far end of that walk
|
|
14
|
+
vi.mock('@jbrowse/core/util', () => ({
|
|
15
|
+
getSession: (model: { session: unknown }) => model.session,
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
function trackModel(session: Record<string, unknown>) {
|
|
19
|
+
return { session } as unknown as AbstractTrackModel
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tiling = { setUseWorkspaces() {}, setPendingMove() {} }
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
localStorage.clear()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
cleanup()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function toggle() {
|
|
33
|
+
return screen.queryByRole('checkbox')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('a host that can tile offers the choice, checked by default', () => {
|
|
37
|
+
render(
|
|
38
|
+
<SubmitCancelActions
|
|
39
|
+
model={trackModel(tiling)}
|
|
40
|
+
onSubmit={() => {}}
|
|
41
|
+
onCancel={() => {}}
|
|
42
|
+
/>,
|
|
43
|
+
)
|
|
44
|
+
expect(toggle()).toBeTruthy()
|
|
45
|
+
expect((toggle() as HTMLInputElement).checked).toBe(true)
|
|
46
|
+
expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// the box would do nothing on an embedded session, and a control that silently
|
|
50
|
+
// does nothing is worse than one that is not there
|
|
51
|
+
test('a host that cannot tile does not offer it', () => {
|
|
52
|
+
render(
|
|
53
|
+
<SubmitCancelActions
|
|
54
|
+
model={trackModel({})}
|
|
55
|
+
onSubmit={() => {}}
|
|
56
|
+
onCancel={() => {}}
|
|
57
|
+
/>,
|
|
58
|
+
)
|
|
59
|
+
expect(toggle()).toBeNull()
|
|
60
|
+
expect(screen.getByText('Submit')).toBeTruthy()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a panel that launches nothing passes no model, and gets no box', () => {
|
|
64
|
+
render(<SubmitCancelActions onSubmit={() => {}} onCancel={() => {}} />)
|
|
65
|
+
expect(toggle()).toBeNull()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('clicking it writes the placement the next launch will read', () => {
|
|
69
|
+
render(
|
|
70
|
+
<SubmitCancelActions
|
|
71
|
+
model={trackModel(tiling)}
|
|
72
|
+
onSubmit={() => {}}
|
|
73
|
+
onCancel={() => {}}
|
|
74
|
+
/>,
|
|
75
|
+
)
|
|
76
|
+
toggle()!.click()
|
|
77
|
+
expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('stack')
|
|
78
|
+
expect((toggle() as HTMLInputElement).checked).toBe(false)
|
|
79
|
+
|
|
80
|
+
toggle()!.click()
|
|
81
|
+
expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('splitRight')
|
|
82
|
+
expect((toggle() as HTMLInputElement).checked).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('a stored choice is what the box opens on', () => {
|
|
86
|
+
localStorage.setItem(LAUNCH_PLACEMENT_KEY, 'stack')
|
|
87
|
+
render(
|
|
88
|
+
<SubmitCancelActions
|
|
89
|
+
model={trackModel(tiling)}
|
|
90
|
+
onSubmit={() => {}}
|
|
91
|
+
onCancel={() => {}}
|
|
92
|
+
/>,
|
|
93
|
+
)
|
|
94
|
+
expect((toggle() as HTMLInputElement).checked).toBe(false)
|
|
95
|
+
})
|
|
@@ -1,6 +1,53 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { getSession } from '@jbrowse/core/util'
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Checkbox,
|
|
7
|
+
DialogActions,
|
|
8
|
+
FormControlLabel,
|
|
9
|
+
} from '@mui/material'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readLaunchPlacement,
|
|
13
|
+
sessionSupportsPlacement,
|
|
14
|
+
writeLaunchPlacement,
|
|
15
|
+
} from '../../utils/workspaces'
|
|
16
|
+
|
|
17
|
+
import type { AbstractTrackModel } from '@jbrowse/core/util'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where the launch puts the view, offered wherever a launch is submitted.
|
|
21
|
+
*
|
|
22
|
+
* A checkbox rather than a menu of the three placements: the choice a reader
|
|
23
|
+
* has at this point is "beside the genome view or under it", and `newTab` is a
|
|
24
|
+
* spec's to state, not a thing to pick before you have seen the alignment.
|
|
25
|
+
*
|
|
26
|
+
* Absent entirely on a host that cannot tile — an embedded session, or a
|
|
27
|
+
* release that places views its own way — because the box would do nothing
|
|
28
|
+
* there and every launch would quietly ignore it.
|
|
29
|
+
*/
|
|
30
|
+
function PlacementToggle({ model }: { model: AbstractTrackModel }) {
|
|
31
|
+
const session = getSession(model)
|
|
32
|
+
const [sideBySide, setSideBySide] = useState(
|
|
33
|
+
() => readLaunchPlacement() === 'splitRight',
|
|
34
|
+
)
|
|
35
|
+
return sessionSupportsPlacement(session) ? (
|
|
36
|
+
<FormControlLabel
|
|
37
|
+
label="Open beside the genome view"
|
|
38
|
+
control={
|
|
39
|
+
<Checkbox
|
|
40
|
+
checked={sideBySide}
|
|
41
|
+
onChange={event => {
|
|
42
|
+
const { checked } = event.target
|
|
43
|
+
setSideBySide(checked)
|
|
44
|
+
writeLaunchPlacement(checked ? 'splitRight' : 'stack')
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
) : null
|
|
50
|
+
}
|
|
4
51
|
|
|
5
52
|
export default function SubmitCancelActions({
|
|
6
53
|
onSubmit,
|
|
@@ -8,34 +55,45 @@ export default function SubmitCancelActions({
|
|
|
8
55
|
submitDisabled,
|
|
9
56
|
submitLabel = 'Submit',
|
|
10
57
|
cancelLabel = 'Cancel',
|
|
58
|
+
model,
|
|
11
59
|
}: {
|
|
12
60
|
onSubmit: () => void
|
|
13
61
|
onCancel: () => void
|
|
14
62
|
submitDisabled?: boolean
|
|
15
63
|
submitLabel?: string
|
|
16
64
|
cancelLabel?: string
|
|
65
|
+
/** omitted by a panel that submits something other than a view launch */
|
|
66
|
+
model?: AbstractTrackModel
|
|
17
67
|
}) {
|
|
18
68
|
return (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
// The buttons are one child rather than two, so a dialog too narrow for
|
|
70
|
+
// the whole row wraps them together underneath the option instead of
|
|
71
|
+
// breaking Cancel away from Submit or shrinking both out of shape.
|
|
72
|
+
<DialogActions sx={{ flexWrap: 'wrap', rowGap: 1 }}>
|
|
73
|
+
{model ? <PlacementToggle model={model} /> : null}
|
|
74
|
+
<div style={{ display: 'flex', gap: 8, marginLeft: 'auto' }}>
|
|
75
|
+
<Button
|
|
76
|
+
sx={{ flexShrink: 0 }}
|
|
77
|
+
color="primary"
|
|
78
|
+
variant="contained"
|
|
79
|
+
disabled={submitDisabled}
|
|
80
|
+
onClick={() => {
|
|
81
|
+
onSubmit()
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{submitLabel}
|
|
85
|
+
</Button>
|
|
86
|
+
<Button
|
|
87
|
+
sx={{ flexShrink: 0 }}
|
|
88
|
+
color="secondary"
|
|
89
|
+
variant="contained"
|
|
90
|
+
onClick={() => {
|
|
91
|
+
onCancel()
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{cancelLabel}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
39
97
|
</DialogActions>
|
|
40
98
|
)
|
|
41
99
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { launchMsaView } from '../utils/launchMsaView'
|
|
2
|
+
|
|
1
3
|
import type { OrthologParams } from '../MsaViewPanel/model'
|
|
4
|
+
import type { MsaViewPlacement } from '../utils/workspaces'
|
|
2
5
|
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
3
6
|
import type { AbstractSessionModel } from '@jbrowse/core/util'
|
|
4
7
|
|
|
@@ -39,6 +42,23 @@ interface LaunchMsaViewArgs {
|
|
|
39
42
|
* snapshot property passes through the same way.
|
|
40
43
|
*/
|
|
41
44
|
allowedGappyness?: number
|
|
45
|
+
/**
|
|
46
|
+
* Where the view lands: `stack` (default), `splitRight` or `newTab`. The
|
|
47
|
+
* declarative half of the launch — a link states the arrangement it wants
|
|
48
|
+
* instead of the reader dragging the view into place.
|
|
49
|
+
*
|
|
50
|
+
* The default is `stack` and has to stay `stack`: it is what every link
|
|
51
|
+
* written before this key existed already does, and the only thing an
|
|
52
|
+
* embedded session can do. The launch DIALOG defaults to `splitRight`
|
|
53
|
+
* instead (see `DEFAULT_LAUNCH_PLACEMENT`), because a launch from a gene
|
|
54
|
+
* feature is a connected pair and reads as a split.
|
|
55
|
+
*
|
|
56
|
+
* A spec that arranges more than these two views should use the host's own
|
|
57
|
+
* `layout` key instead, which states the whole tree and is applied after
|
|
58
|
+
* every view in the spec has launched. Both may be given; `layout` wins,
|
|
59
|
+
* being the later and more specific statement.
|
|
60
|
+
*/
|
|
61
|
+
placement?: MsaViewPlacement
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
export default function LaunchMsaViewExtensionPointF(
|
|
@@ -76,8 +96,7 @@ export default function LaunchMsaViewExtensionPointF(
|
|
|
76
96
|
// directly, and so is orthologParams (the model's own autorun picks it up).
|
|
77
97
|
// Only sources needing launch-time resolution go through `init`: msaUrl
|
|
78
98
|
// (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
|
|
79
|
-
session
|
|
80
|
-
type: 'MsaView',
|
|
99
|
+
launchMsaView(session, {
|
|
81
100
|
...rest,
|
|
82
101
|
data,
|
|
83
102
|
...(treeFileLocation
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
fetchProteinForGene,
|
|
9
9
|
resolveGeneId,
|
|
10
10
|
} from '../utils/ncbiOrthologs'
|
|
11
|
+
import { fetchPantherOrthologs } from '../utils/pantherOrthologs'
|
|
11
12
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames'
|
|
12
13
|
|
|
13
14
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
@@ -24,12 +25,16 @@ vi.mock('../utils/ncbiOrthologs', async importOriginal => ({
|
|
|
24
25
|
fetchProteinForGene: vi.fn(),
|
|
25
26
|
fetchOrthologRows: vi.fn(),
|
|
26
27
|
}))
|
|
28
|
+
vi.mock('../utils/pantherOrthologs', () => ({
|
|
29
|
+
fetchPantherOrthologs: vi.fn(),
|
|
30
|
+
}))
|
|
27
31
|
vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }))
|
|
28
32
|
vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }))
|
|
29
33
|
|
|
30
34
|
const mockResolveGeneId = vi.mocked(resolveGeneId)
|
|
31
35
|
const mockFetchProtein = vi.mocked(fetchProteinForGene)
|
|
32
36
|
const mockFetchRows = vi.mocked(fetchOrthologRows)
|
|
37
|
+
const mockFetchPanther = vi.mocked(fetchPantherOrthologs)
|
|
33
38
|
const mockLaunchMSA = vi.mocked(launchMSA)
|
|
34
39
|
const mockFetchTaxonomy = vi.mocked(fetchTaxonomyInfo)
|
|
35
40
|
|
|
@@ -245,3 +250,115 @@ describe('the Accession that drives the domain overlay', () => {
|
|
|
245
250
|
expect(queryMetadata(result).Accession).toBeUndefined()
|
|
246
251
|
})
|
|
247
252
|
})
|
|
253
|
+
|
|
254
|
+
// The second source. What is under test is the dispatch and what the PANTHER
|
|
255
|
+
// result becomes on the query row -- the rows themselves are shaped upstream,
|
|
256
|
+
// and the tail of the launch (labels, aligner, metadata) is the same code the
|
|
257
|
+
// NCBI tests above already cover.
|
|
258
|
+
describe('the PANTHER source', () => {
|
|
259
|
+
const YEAST = 559292
|
|
260
|
+
const found = {
|
|
261
|
+
matched: 'CDC28',
|
|
262
|
+
query: {
|
|
263
|
+
code: 'YEAST',
|
|
264
|
+
accession: 'P00546',
|
|
265
|
+
geneRef: 'SGD=S000000364',
|
|
266
|
+
sequence: 'MSGELANYKRLEKVGEGTYGVVYKA',
|
|
267
|
+
},
|
|
268
|
+
rows: [
|
|
269
|
+
{
|
|
270
|
+
taxId: HUMAN,
|
|
271
|
+
label: 'human',
|
|
272
|
+
scientificName: 'Homo sapiens',
|
|
273
|
+
commonName: 'human',
|
|
274
|
+
geneId: 'HGNC=1771',
|
|
275
|
+
protein: 'P24941',
|
|
276
|
+
sequence: 'MENFQKVEKIGEGTYGVVYKARNK',
|
|
277
|
+
},
|
|
278
|
+
] as OrthologRow[],
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
mockFetchPanther.mockResolvedValue(found)
|
|
283
|
+
mockFetchTaxonomy.mockResolvedValue(
|
|
284
|
+
new Map([[YEAST, { sciname: 'Saccharomyces cerevisiae' }]]),
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('source omitted is NCBI, so an old launch never reaches PANTHER', async () => {
|
|
289
|
+
await doLaunchOrthologs({ self: makeModel(params()) })
|
|
290
|
+
expect(mockFetchPanther).not.toHaveBeenCalled()
|
|
291
|
+
expect(mockResolveGeneId).toHaveBeenCalled()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('source panther asks PANTHER with the same species semantics, and skips NCBI', async () => {
|
|
295
|
+
await doLaunchOrthologs({
|
|
296
|
+
self: makeModel({
|
|
297
|
+
taxId: YEAST,
|
|
298
|
+
source: 'panther',
|
|
299
|
+
geneCandidates: ['CDC28'],
|
|
300
|
+
msaAlgorithm: 'clustalo',
|
|
301
|
+
taxa: [HUMAN, YEAST],
|
|
302
|
+
maxSpecies: 7,
|
|
303
|
+
}),
|
|
304
|
+
})
|
|
305
|
+
expect(mockResolveGeneId).not.toHaveBeenCalled()
|
|
306
|
+
expect(mockFetchRows).not.toHaveBeenCalled()
|
|
307
|
+
const { candidates, taxId, taxa, exclude, limit } =
|
|
308
|
+
mockFetchPanther.mock.calls[0]![0]
|
|
309
|
+
expect(candidates).toEqual(['CDC28'])
|
|
310
|
+
expect(taxId).toBe(YEAST)
|
|
311
|
+
expect([...taxa!]).toEqual([HUMAN, YEAST])
|
|
312
|
+
expect(exclude).toBe(YEAST)
|
|
313
|
+
expect(limit).toBe(7)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test("the query row is PANTHER's own entry for the gene when no sequence was supplied, and carries its UniProt accession for the domain overlay", async () => {
|
|
317
|
+
const result = await doLaunchOrthologs({
|
|
318
|
+
self: makeModel({
|
|
319
|
+
taxId: YEAST,
|
|
320
|
+
source: 'panther',
|
|
321
|
+
geneCandidates: ['CDC28'],
|
|
322
|
+
msaAlgorithm: 'clustalo',
|
|
323
|
+
}),
|
|
324
|
+
})
|
|
325
|
+
expect(queryRowName()).toBe('Saccharomyces_cerevisiae_query')
|
|
326
|
+
expect(queryRowSent()).toBe(found.query.sequence)
|
|
327
|
+
expect(queryMetadata(result)).toEqual({
|
|
328
|
+
'Gene ID': 'SGD=S000000364',
|
|
329
|
+
Accession: 'P00546',
|
|
330
|
+
})
|
|
331
|
+
expect(JSON.parse(result.treeMetadata).human).toMatchObject({
|
|
332
|
+
Accession: 'P24941',
|
|
333
|
+
'Gene ID': 'HGNC=1771',
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test('a supplied sequence still wins, and a different isoform earns no Accession', async () => {
|
|
338
|
+
const result = await doLaunchOrthologs({
|
|
339
|
+
self: makeModel({
|
|
340
|
+
taxId: YEAST,
|
|
341
|
+
source: 'panther',
|
|
342
|
+
geneCandidates: ['CDC28'],
|
|
343
|
+
msaAlgorithm: 'clustalo',
|
|
344
|
+
proteinSequence: 'MDIFFERENTISOFORM',
|
|
345
|
+
}),
|
|
346
|
+
})
|
|
347
|
+
expect(queryRowSent()).toBe('MDIFFERENTISOFORM')
|
|
348
|
+
expect(queryMetadata(result).Accession).toBeUndefined()
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('names PANTHER when it has no protein for the query row', async () => {
|
|
352
|
+
mockFetchPanther.mockResolvedValue({ ...found, query: undefined })
|
|
353
|
+
await expect(
|
|
354
|
+
doLaunchOrthologs({
|
|
355
|
+
self: makeModel({
|
|
356
|
+
taxId: YEAST,
|
|
357
|
+
source: 'panther',
|
|
358
|
+
geneCandidates: ['CDC28'],
|
|
359
|
+
msaAlgorithm: 'clustalo',
|
|
360
|
+
}),
|
|
361
|
+
}),
|
|
362
|
+
).rejects.toThrow(/PANTHER returned no representative protein/)
|
|
363
|
+
})
|
|
364
|
+
})
|