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.
Files changed (57) hide show
  1. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +1 -1
  2. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  3. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +6 -4
  4. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  5. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +4 -2
  6. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +8 -3
  7. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
  8. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
  9. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +4 -2
  10. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +1 -1
  11. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +4 -2
  12. package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +4 -1
  13. package/dist/LaunchMsaView/components/SubmitCancelActions.js +34 -10
  14. package/dist/LaunchMsaView/components/SubmitCancelActions.test.d.ts +1 -0
  15. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +55 -0
  16. package/dist/LaunchMsaViewExtensionPoint/index.js +2 -2
  17. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
  18. package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
  19. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +105 -0
  20. package/dist/MsaViewPanel/model.d.ts +9 -0
  21. package/dist/MsaViewPanel/model.js +6 -0
  22. package/dist/index.js +4 -1
  23. package/dist/jbrowse-plugin-msaview.umd.production.min.js +30 -30
  24. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  25. package/dist/utils/launchMsaView.d.ts +19 -0
  26. package/dist/utils/launchMsaView.js +13 -0
  27. package/dist/utils/pantherOrthologs.d.ts +79 -0
  28. package/dist/utils/pantherOrthologs.js +262 -0
  29. package/dist/utils/workspaces.d.ts +34 -0
  30. package/dist/utils/workspaces.js +100 -0
  31. package/dist/utils/workspaces.test.d.ts +1 -0
  32. package/dist/utils/workspaces.test.js +100 -0
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +4 -1
  36. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +1 -0
  37. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
  38. package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +7 -4
  39. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
  40. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +5 -2
  41. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +20 -4
  42. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
  43. package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +5 -2
  44. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +1 -0
  45. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +5 -2
  46. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +95 -0
  47. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +80 -22
  48. package/src/LaunchMsaViewExtensionPoint/index.ts +21 -2
  49. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +117 -0
  50. package/src/MsaViewPanel/doLaunchOrthologs.ts +99 -37
  51. package/src/MsaViewPanel/model.ts +10 -0
  52. package/src/index.ts +4 -1
  53. package/src/utils/launchMsaView.ts +30 -0
  54. package/src/utils/pantherOrthologs.ts +399 -0
  55. package/src/utils/workspaces.test.ts +132 -0
  56. package/src/utils/workspaces.ts +146 -0
  57. 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).addView('MsaView', {
20
- type: 'MsaView',
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).addView('MsaView', {
43
- type: 'MsaView',
45
+ launchMsaView(getSession(view), {
46
+ placement: readLaunchPlacement(),
44
47
  displayName: newViewTitle,
45
48
  connectedViewId: view.id,
46
49
  connectedFeature,
@@ -137,6 +137,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({
137
137
  </LaunchPanelContent>
138
138
 
139
139
  <SubmitCancelActions
140
+ model={model}
140
141
  submitDisabled={
141
142
  !selectedTranscript ||
142
143
  (inputMethod === 'file' && !msaFileLocation) ||
@@ -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).addView('MsaView', {
27
- type: 'MsaView',
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
- NCBI&apos;s precomputed orthologs, one gene per species, looked up
69
- rather than searched for. No BLAST job to queue.
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="the closest N species NCBI has"
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).addView('MsaView', {
19
- type: 'MsaView',
21
+ launchMsaView(getSession(view), {
22
+ placement: readLaunchPlacement(),
20
23
  displayName: newViewTitle,
21
24
  connectedViewId: view.id,
22
25
  connectedFeature: feature.toJSON(),
@@ -130,6 +130,7 @@ const PreLoadedMSA = observer(function ({
130
130
  </LaunchPanelContent>
131
131
 
132
132
  <SubmitCancelActions
133
+ model={model}
133
134
  submitDisabled={!selectedTranscript || !msaData?.length}
134
135
  onSubmit={() => {
135
136
  try {
@@ -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).addView('MsaView', {
20
- type: 'MsaView',
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 { Button, DialogActions } from '@mui/material'
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
- <DialogActions>
20
- <Button
21
- color="primary"
22
- variant="contained"
23
- disabled={submitDisabled}
24
- onClick={() => {
25
- onSubmit()
26
- }}
27
- >
28
- {submitLabel}
29
- </Button>
30
- <Button
31
- color="secondary"
32
- variant="contained"
33
- onClick={() => {
34
- onCancel()
35
- }}
36
- >
37
- {cancelLabel}
38
- </Button>
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.addView('MsaView', {
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
+ })