jbrowse-plugin-msaview 3.0.0 → 3.2.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 (67) hide show
  1. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +1 -1
  2. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.d.ts +11 -0
  3. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +73 -16
  4. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +6 -4
  5. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +8 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/consts.js +8 -1
  7. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +8 -15
  8. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +4 -2
  9. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +1 -1
  10. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +4 -2
  11. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +1 -1
  12. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +4 -2
  13. package/dist/LaunchMsaView/components/QueryRowSelector.d.ts +16 -0
  14. package/dist/LaunchMsaView/components/QueryRowSelector.js +38 -0
  15. package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +4 -1
  16. package/dist/LaunchMsaView/components/SubmitCancelActions.js +34 -10
  17. package/dist/LaunchMsaView/components/SubmitCancelActions.test.d.ts +1 -0
  18. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +55 -0
  19. package/dist/LaunchMsaView/detectQueryRow.d.ts +23 -0
  20. package/dist/LaunchMsaView/detectQueryRow.js +94 -0
  21. package/dist/LaunchMsaView/detectQueryRow.test.d.ts +1 -0
  22. package/dist/LaunchMsaView/detectQueryRow.test.js +65 -0
  23. package/dist/LaunchMsaView/useQueryRowName.d.ts +15 -0
  24. package/dist/LaunchMsaView/useQueryRowName.js +26 -0
  25. package/dist/LaunchMsaViewExtensionPoint/index.js +2 -2
  26. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +27 -11
  27. package/dist/MsaViewPanel/afterCreateAutoruns.js +96 -47
  28. package/dist/MsaViewPanel/observeProteinHighlights.test.d.ts +1 -0
  29. package/dist/MsaViewPanel/observeProteinHighlights.test.js +209 -0
  30. package/dist/MsaViewPanel/structureConnection.d.ts +6 -0
  31. package/dist/index.js +4 -1
  32. package/dist/jbrowse-plugin-msaview.umd.production.min.js +28 -28
  33. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  34. package/dist/utils/launchMsaView.d.ts +19 -0
  35. package/dist/utils/launchMsaView.js +13 -0
  36. package/dist/utils/workspaces.d.ts +34 -0
  37. package/dist/utils/workspaces.js +100 -0
  38. package/dist/utils/workspaces.test.d.ts +1 -0
  39. package/dist/utils/workspaces.test.js +100 -0
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +6 -1
  43. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +1 -0
  44. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +127 -30
  45. package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +7 -4
  46. package/src/LaunchMsaView/components/BlastQuery/consts.ts +8 -1
  47. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +8 -37
  48. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +5 -2
  49. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +1 -1
  50. package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +5 -2
  51. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +1 -0
  52. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +5 -2
  53. package/src/LaunchMsaView/components/QueryRowSelector.tsx +93 -0
  54. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +95 -0
  55. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +80 -22
  56. package/src/LaunchMsaView/detectQueryRow.test.ts +79 -0
  57. package/src/LaunchMsaView/detectQueryRow.ts +132 -0
  58. package/src/LaunchMsaView/useQueryRowName.ts +33 -0
  59. package/src/LaunchMsaViewExtensionPoint/index.ts +21 -2
  60. package/src/MsaViewPanel/afterCreateAutoruns.ts +106 -51
  61. package/src/MsaViewPanel/observeProteinHighlights.test.ts +264 -0
  62. package/src/MsaViewPanel/structureConnection.ts +7 -0
  63. package/src/index.ts +4 -1
  64. package/src/utils/launchMsaView.ts +30 -0
  65. package/src/utils/workspaces.test.ts +132 -0
  66. package/src/utils/workspaces.ts +146 -0
  67. package/src/version.ts +1 -1
@@ -0,0 +1,19 @@
1
+ import type { MsaViewPlacement } from './workspaces';
2
+ import type { AbstractSessionModel } from '@jbrowse/core/util';
3
+ /**
4
+ * A launch, stated: what the view is, and where it goes. Everything but
5
+ * `placement` is a react-msaview or plugin-model snapshot property, passed
6
+ * through untouched so this never becomes a list that has to grow.
7
+ */
8
+ export interface MsaViewLaunchSpec extends Record<string, unknown> {
9
+ /** default `stack`, the only thing an embedded session can do */
10
+ placement?: MsaViewPlacement;
11
+ }
12
+ /**
13
+ * The one place a launch adds an MSA view -- the dialog's four tabs, the Add
14
+ * menu, and the `LaunchView-MsaView` extension point a session spec arrives on
15
+ * all come through here. Each of them used to run its own `addView` and none
16
+ * placed the result, which is how a launch from a gene feature landed stacked
17
+ * under the very genome view it was connected to.
18
+ */
19
+ export declare function launchMsaView(session: AbstractSessionModel, { placement, ...snapshot }: MsaViewLaunchSpec): import("@jbrowse/core/util").AbstractViewModel;
@@ -0,0 +1,13 @@
1
+ import { placeMsaView } from './workspaces';
2
+ /**
3
+ * The one place a launch adds an MSA view -- the dialog's four tabs, the Add
4
+ * menu, and the `LaunchView-MsaView` extension point a session spec arrives on
5
+ * all come through here. Each of them used to run its own `addView` and none
6
+ * placed the result, which is how a launch from a gene feature landed stacked
7
+ * under the very genome view it was connected to.
8
+ */
9
+ export function launchMsaView(session, { placement = 'stack', ...snapshot }) {
10
+ const view = session.addView('MsaView', { type: 'MsaView', ...snapshot });
11
+ placeMsaView(session, view.id, placement);
12
+ return view;
13
+ }
@@ -0,0 +1,34 @@
1
+ import type { AbstractSessionModel } from '@jbrowse/core/util';
2
+ /**
3
+ * Where a launched MSA view lands. Every launch names one of these and stops
4
+ * there, so a host that arranges views differently is one function to teach.
5
+ *
6
+ * stack append below whatever is on screen
7
+ * splitRight its own cell to the right, beside the view it is connected to
8
+ * newTab its own tab in the current cell
9
+ */
10
+ export type MsaViewPlacement = 'stack' | 'splitRight' | 'newTab';
11
+ /**
12
+ * What the dialog does unasked. Side-by-side, because a launch from a gene
13
+ * feature sets `connectedViewId`: the pair shares a hover and a highlight, and
14
+ * reads as a split. A session spec defaults to `stack` instead.
15
+ */
16
+ export declare const DEFAULT_LAUNCH_PLACEMENT: MsaViewPlacement;
17
+ export declare const LAUNCH_PLACEMENT_KEY = "msaView-launchPlacement";
18
+ export declare function resetWorkspacesWarning(): void;
19
+ /**
20
+ * Whether this host can honor anything other than `stack`. Silent: the dialog
21
+ * asks on every render, and only a launch is worth warning about.
22
+ */
23
+ export declare function sessionSupportsPlacement(session: AbstractSessionModel): boolean;
24
+ /**
25
+ * Put a freshly added view where the launch said to. `stack` is a placement
26
+ * rather than the absence of one, so no caller has to ask what host it is on.
27
+ */
28
+ export declare function placeMsaView(session: AbstractSessionModel, viewId: string, placement: MsaViewPlacement): void;
29
+ /**
30
+ * The dialog's own remembered choice — not the host's preferences system, which
31
+ * records whether the user likes workspaces and does not exist everywhere.
32
+ */
33
+ export declare function readLaunchPlacement(): MsaViewPlacement;
34
+ export declare function writeLaunchPlacement(placement: MsaViewPlacement): void;
@@ -0,0 +1,100 @@
1
+ const PLACEMENTS = ['stack', 'splitRight', 'newTab'];
2
+ /**
3
+ * What the dialog does unasked. Side-by-side, because a launch from a gene
4
+ * feature sets `connectedViewId`: the pair shares a hover and a highlight, and
5
+ * reads as a split. A session spec defaults to `stack` instead.
6
+ */
7
+ export const DEFAULT_LAUNCH_PLACEMENT = 'splitRight';
8
+ export const LAUNCH_PLACEMENT_KEY = 'msaView-launchPlacement';
9
+ function hasAction(session, name) {
10
+ return (name in session &&
11
+ typeof session[name] === 'function');
12
+ }
13
+ // Warned at most once. This is a property of the host, so the answer is the
14
+ // same on every launch and a dialog the user reopens should not stack up noise.
15
+ let warnedPartial = false;
16
+ export function resetWorkspacesWarning() {
17
+ warnedPartial = false;
18
+ }
19
+ /**
20
+ * Whether this host can honor anything other than `stack`. Silent: the dialog
21
+ * asks on every render, and only a launch is worth warning about.
22
+ */
23
+ export function sessionSupportsPlacement(session) {
24
+ return (hasAction(session, 'setUseWorkspaces') &&
25
+ hasAction(session, 'setPendingMove'));
26
+ }
27
+ function isSessionWithWorkspaces(session) {
28
+ const canEnable = hasAction(session, 'setUseWorkspaces');
29
+ const canPlace = hasAction(session, 'setPendingMove');
30
+ // Missing BOTH is an embedded session: it has no workspaces, there is nothing
31
+ // to ask for, and silence is the right answer.
32
+ //
33
+ // Missing ONE is a host that has workspaces but places views some other way,
34
+ // and silence there is how the same feature broke in jbrowse-plugin-protein3d
35
+ // — jbrowse-web folded `setPendingMove` into its layout `init`, the guard went
36
+ // false, and the plugin simply stopped asking: no error, no missing feature,
37
+ // two views quietly stacking, nobody noticed for weeks. Feature detection
38
+ // cannot ask a host to announce a change, but it can tell "not supported here"
39
+ // from "supported, and gone".
40
+ //
41
+ // Two very different hosts produce this one shape and nothing on the session
42
+ // tells them apart, so the message carries both rather than a guess:
43
+ //
44
+ // - releases through v4.3.0, where placement is `setPendingMoveToSplitRight`,
45
+ // a module function in @jbrowse/app-core rather than a session action.
46
+ // Nothing is wrong and nothing needs fixing
47
+ // - a newer host that moved the action out from under us, which is the
48
+ // regression this warning exists to catch
49
+ //
50
+ // Do not quiet the first case by sniffing the version. The alarm is only worth
51
+ // having if it fires on a shape it cannot explain, and these two are identical.
52
+ if (canEnable !== canPlace && !warnedPartial) {
53
+ warnedPartial = true;
54
+ console.warn(`jbrowse-plugin-msaview: this session supports workspaces but not ` +
55
+ `${canPlace ? 'setUseWorkspaces' : 'setPendingMove'}, so the MSA view ` +
56
+ `was stacked instead of tiled. Expected on releases through v4.3.0, ` +
57
+ `which place views through @jbrowse/app-core instead; on a newer host ` +
58
+ `it means the session API moved and the plugin needs updating to match.`);
59
+ }
60
+ return canEnable && canPlace;
61
+ }
62
+ /**
63
+ * Put a freshly added view where the launch said to. `stack` is a placement
64
+ * rather than the absence of one, so no caller has to ask what host it is on.
65
+ */
66
+ export function placeMsaView(session, viewId, placement) {
67
+ if (placement === 'stack' || !isSessionWithWorkspaces(session)) {
68
+ return;
69
+ }
70
+ session.setPendingMove({ type: placement, viewId });
71
+ // Session-scoped: turning workspaces on for this session leaves the user's
72
+ // own default alone, which is what `setUseWorkspaces` (as against
73
+ // `setUseWorkspacesPreference`) is for.
74
+ session.setUseWorkspaces(true);
75
+ }
76
+ function isPlacement(value) {
77
+ return PLACEMENTS.includes(value);
78
+ }
79
+ /**
80
+ * The dialog's own remembered choice — not the host's preferences system, which
81
+ * records whether the user likes workspaces and does not exist everywhere.
82
+ */
83
+ export function readLaunchPlacement() {
84
+ try {
85
+ const stored = globalThis.localStorage.getItem(LAUNCH_PLACEMENT_KEY);
86
+ return isPlacement(stored) ? stored : DEFAULT_LAUNCH_PLACEMENT;
87
+ }
88
+ catch (error) {
89
+ console.error(error);
90
+ return DEFAULT_LAUNCH_PLACEMENT;
91
+ }
92
+ }
93
+ export function writeLaunchPlacement(placement) {
94
+ try {
95
+ globalThis.localStorage.setItem(LAUNCH_PLACEMENT_KEY, placement);
96
+ }
97
+ catch (error) {
98
+ console.error(error);
99
+ }
100
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
1
+ import { afterEach, beforeEach, expect, test, vi } from 'vitest';
2
+ import { launchMsaView } from './launchMsaView';
3
+ import { DEFAULT_LAUNCH_PLACEMENT, LAUNCH_PLACEMENT_KEY, placeMsaView, readLaunchPlacement, resetWorkspacesWarning, sessionSupportsPlacement, writeLaunchPlacement, } from './workspaces';
4
+ function makeSession({ canPlace = true, canEnable = true, } = {}) {
5
+ const recorded = { moves: [], workspaces: [], added: [] };
6
+ const session = {
7
+ addView(type, snapshot) {
8
+ recorded.added.push({ type, snapshot });
9
+ return { id: `view-${recorded.added.length}` };
10
+ },
11
+ };
12
+ if (canPlace) {
13
+ session.setPendingMove = (move) => recorded.moves.push(move);
14
+ }
15
+ if (canEnable) {
16
+ session.setUseWorkspaces = (on) => recorded.workspaces.push(on);
17
+ }
18
+ return { session: session, recorded };
19
+ }
20
+ // node has no localStorage, and the plugin runs in a browser -- a Map-backed
21
+ // stub keeps read/write round-tripping without pulling in jsdom
22
+ function stubStorage() {
23
+ const store = new Map();
24
+ vi.stubGlobal('localStorage', {
25
+ getItem: (key) => store.get(key) ?? null,
26
+ setItem: (key, value) => store.set(key, value),
27
+ });
28
+ }
29
+ beforeEach(() => {
30
+ resetWorkspacesWarning();
31
+ stubStorage();
32
+ });
33
+ afterEach(() => {
34
+ vi.restoreAllMocks();
35
+ vi.unstubAllGlobals();
36
+ });
37
+ test('stack places nothing, on a host that could tile', () => {
38
+ const { session, recorded } = makeSession();
39
+ placeMsaView(session, 'view-1', 'stack');
40
+ expect(recorded.moves).toEqual([]);
41
+ expect(recorded.workspaces).toEqual([]);
42
+ });
43
+ test('splitRight asks for the move, then turns workspaces on', () => {
44
+ const { session, recorded } = makeSession();
45
+ placeMsaView(session, 'view-1', 'splitRight');
46
+ expect(recorded.moves).toEqual([{ type: 'splitRight', viewId: 'view-1' }]);
47
+ expect(recorded.workspaces).toEqual([true]);
48
+ });
49
+ test('newTab is the same path with the other move type', () => {
50
+ const { session, recorded } = makeSession();
51
+ placeMsaView(session, 'view-1', 'newTab');
52
+ expect(recorded.moves).toEqual([{ type: 'newTab', viewId: 'view-1' }]);
53
+ });
54
+ // an embedded session has no workspaces at all, so there is nothing to report
55
+ test('a session with neither action is a silent no-op', () => {
56
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
57
+ const { session, recorded } = makeSession({
58
+ canPlace: false,
59
+ canEnable: false,
60
+ });
61
+ placeMsaView(session, 'view-1', 'splitRight');
62
+ expect(recorded.moves).toEqual([]);
63
+ expect(warn).not.toHaveBeenCalled();
64
+ expect(sessionSupportsPlacement(session)).toBe(false);
65
+ });
66
+ // the shape that broke jbrowse-plugin-protein3d silently: workspaces are there,
67
+ // the action this plugin reaches for is not
68
+ test('a half-supported host warns once and stacks', () => {
69
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
70
+ const { session, recorded } = makeSession({ canPlace: false });
71
+ placeMsaView(session, 'view-1', 'splitRight');
72
+ placeMsaView(session, 'view-2', 'splitRight');
73
+ expect(recorded.moves).toEqual([]);
74
+ expect(warn).toHaveBeenCalledTimes(1);
75
+ expect(warn.mock.calls[0]?.[0]).toContain('setPendingMove');
76
+ });
77
+ test('the dialog default is side-by-side, and a junk value falls back to it', () => {
78
+ expect(DEFAULT_LAUNCH_PLACEMENT).toBe('splitRight');
79
+ expect(readLaunchPlacement()).toBe('splitRight');
80
+ localStorage.setItem(LAUNCH_PLACEMENT_KEY, 'sideways');
81
+ expect(readLaunchPlacement()).toBe('splitRight');
82
+ writeLaunchPlacement('stack');
83
+ expect(readLaunchPlacement()).toBe('stack');
84
+ });
85
+ test('launchMsaView defaults to stack, so a spec written before placement existed is unchanged', () => {
86
+ const { session, recorded } = makeSession();
87
+ launchMsaView(session, { data: { msa: '>a\nAC' } });
88
+ expect(recorded.added).toEqual([
89
+ { type: 'MsaView', snapshot: { type: 'MsaView', data: { msa: '>a\nAC' } } },
90
+ ]);
91
+ expect(recorded.moves).toEqual([]);
92
+ });
93
+ // placement is a launch instruction, not view state: MST would drop it from the
94
+ // snapshot without a word, and the view would land stacked with nothing said
95
+ test('launchMsaView keeps placement out of the view snapshot', () => {
96
+ const { session, recorded } = makeSession();
97
+ launchMsaView(session, { placement: 'splitRight', colWidth: 10 });
98
+ expect(recorded.added[0]?.snapshot).toEqual({ type: 'MsaView', colWidth: 10 });
99
+ expect(recorded.moves).toEqual([{ type: 'splitRight', viewId: 'view-1' }]);
100
+ });
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "3.0.0";
1
+ export declare const version = "3.2.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '3.0.0';
1
+ export const version = '3.2.0';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.0.0",
2
+ "version": "3.2.0",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-msaview",
5
5
  "repository": {
@@ -32,6 +32,8 @@
32
32
  "@mui/material": "^9.2.0",
33
33
  "@mui/system": "^9.2.0",
34
34
  "@mui/x-data-grid": "^9.10.0",
35
+ "@testing-library/dom": "^10.4.1",
36
+ "@testing-library/react": "^16.3.2",
35
37
  "@types/node": "^26.1.1",
36
38
  "@types/react": "^19.2.17",
37
39
  "esbuild": "^0.28.1",
@@ -41,6 +43,7 @@
41
43
  "eslint-plugin-react-hooks": "^7.1.1",
42
44
  "eslint-plugin-unicorn": "^72.0.0",
43
45
  "git-cliff": "^2.13.1",
46
+ "jsdom": "^30.0.1",
44
47
  "mobx": "^6.16.1",
45
48
  "mobx-react": "^9.2.2",
46
49
  "msa-parsers": "^6.0.0",
@@ -68,6 +71,8 @@
68
71
  "prebuild": "pnpm clean",
69
72
  "lint": "eslint src --report-unused-disable-directives --max-warnings 0",
70
73
  "check-text-source": "node scripts/check-text-source.mjs",
74
+ "check-ebi-params": "node scripts/check-ebi-params.mjs",
75
+ "check-mui-imports": "node scripts/check-mui-imports.mjs",
71
76
  "pretest": "rm -rf .test-jbrowse && npx @jbrowse/cli create .test-jbrowse --nightly",
72
77
  "test": "vitest run",
73
78
  "test:watch": "vitest",
@@ -123,6 +123,7 @@ const BlastAutomaticPanel = observer(function ({
123
123
  ) : null}
124
124
  </LaunchPanelContent>
125
125
  <SubmitCancelActions
126
+ model={model}
126
127
  submitDisabled={!proteinSequence}
127
128
  onSubmit={() => {
128
129
  try {
@@ -1,14 +1,23 @@
1
- import React from 'react'
1
+ import React, { useState } from 'react'
2
2
 
3
3
  import { shorten2 } from '@jbrowse/core/util'
4
- import { Button, DialogActions, Typography } from '@mui/material'
4
+ import { Alert, Typography } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
6
  import { makeStyles } from 'tss-react/mui'
7
7
 
8
8
  import { BASE_BLAST_URL } from './consts'
9
9
  import ExternalLink from '../../../components/ExternalLink'
10
- import { cleanProteinSequence, getLinearGenomeView } from '../../util'
10
+ import TextField2 from '../../../components/TextField2'
11
+ import { useQueryRowName } from '../../useQueryRowName'
12
+ import {
13
+ cleanProteinSequence,
14
+ getGeneDisplayName,
15
+ getLinearGenomeView,
16
+ } from '../../util'
11
17
  import LaunchPanelContent from '../LaunchPanelContent'
18
+ import { launchView } from '../ManualMSALoader/launchView'
19
+ import QueryRowSelector from '../QueryRowSelector'
20
+ import SubmitCancelActions from '../SubmitCancelActions'
12
21
  import TranscriptSelector from '../TranscriptSelector'
13
22
  import { useTranscriptSelection } from '../useTranscriptSelection'
14
23
 
@@ -17,14 +26,33 @@ import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
17
26
  const useStyles = makeStyles()({
18
27
  ncbiLink: {
19
28
  wordBreak: 'break-all',
20
- margin: 30,
21
- maxWidth: 600,
22
29
  },
23
- infoText: {
30
+ textAreaFont: {
31
+ fontFamily: 'Courier New',
32
+ },
33
+ msaInput: {
34
+ marginBottom: 20,
35
+ },
36
+ step: {
24
37
  marginTop: 20,
25
38
  },
39
+ stepBody: {
40
+ marginLeft: 20,
41
+ marginTop: 8,
42
+ },
26
43
  })
27
44
 
45
+ /**
46
+ * The route to NCBI's `nr`, which no plugin version can query directly: NCBI
47
+ * stopped sending Access-Control-Allow-Origin to third-party origins, so the
48
+ * browser cannot read Blast.cgi at all (see docs/blast.md).
49
+ *
50
+ * That makes the round trip through NCBI's own site the whole feature rather
51
+ * than a fallback, so the panel walks it end to end. It used to hand the user a
52
+ * link, tell them to "paste the results into JBrowse", and offer only a Close
53
+ * button -- leaving them to find the Manual upload tab, re-pick the transcript
54
+ * they had already chosen here, and hand-type the row name.
55
+ */
28
56
  const BlastManualPanel = observer(function ({
29
57
  handleClose,
30
58
  feature,
@@ -38,8 +66,13 @@ const BlastManualPanel = observer(function ({
38
66
  }) {
39
67
  const { classes } = useStyles()
40
68
  const view = getLinearGenomeView(model)
69
+ const [launchViewError, setLaunchViewError] = useState<unknown>()
70
+ const [msaText, setMsaText] = useState('')
71
+ const [treeText, setTreeText] = useState('')
72
+
41
73
  const transcriptSelection = useTranscriptSelection({ feature, view })
42
- const { proteinSequence, error } = transcriptSelection
74
+ const { proteinSequence, selectedTranscript, error } = transcriptSelection
75
+ const queryRow = useQueryRowName(msaText, proteinSequence)
43
76
 
44
77
  const s2 = cleanProteinSequence(proteinSequence)
45
78
  // a link the user follows to NCBI's own site, not something we fetch — which
@@ -49,37 +82,101 @@ const BlastManualPanel = observer(function ({
49
82
 
50
83
  return (
51
84
  <>
52
- <LaunchPanelContent error={error}>
85
+ <LaunchPanelContent error={launchViewError ?? error}>
53
86
  {children}
54
87
 
55
88
  <TranscriptSelector feature={feature} {...transcriptSelection} />
56
89
 
57
- {proteinSequence ? (
58
- <div className={classes.ncbiLink}>
59
- Link to NCBI BLAST: <ExternalLink href={link}>{link2}</ExternalLink>
90
+ <div className={classes.step}>
91
+ <Typography variant="subtitle2">1. Run the search at NCBI</Typography>
92
+ <div className={classes.stepBody}>
93
+ {proteinSequence ? (
94
+ <div className={classes.ncbiLink}>
95
+ <ExternalLink href={link}>{link2}</ExternalLink>
96
+ </div>
97
+ ) : (
98
+ <Alert severity="info">
99
+ Pick a transcript above to get a link carrying its protein
100
+ sequence.
101
+ </Alert>
102
+ )}
60
103
  </div>
61
- ) : null}
104
+ </div>
62
105
 
63
- <Typography className={classes.infoText}>
64
- Click the link above and run your BLAST query, and once you have
65
- results, click "Multiple Alignment" at the top of the results page to
66
- be redirected to COBALT, NCBI's multiple sequence aligner. Once COBALT
67
- completes, you can download an MSA (.aln file) and optionally a Newick
68
- tree (.nh) and paste the results into JBrowse
69
- </Typography>
106
+ <div className={classes.step}>
107
+ <Typography variant="subtitle2">2. Align the hits</Typography>
108
+ <div className={classes.stepBody}>
109
+ <Typography>
110
+ On the results page click "Multiple Alignment" to run COBALT,
111
+ NCBI's aligner. Download the alignment (.aln) and, if you want the
112
+ tree drawn, the Newick tree (.nh).
113
+ </Typography>
114
+ </div>
115
+ </div>
116
+
117
+ <div className={classes.step}>
118
+ <Typography variant="subtitle2">
119
+ 3. Paste the results back here
120
+ </Typography>
121
+ <div className={classes.stepBody}>
122
+ <TextField2
123
+ variant="outlined"
124
+ label="Alignment"
125
+ multiline
126
+ minRows={5}
127
+ maxRows={10}
128
+ fullWidth
129
+ className={classes.msaInput}
130
+ slotProps={{ input: { className: classes.textAreaFont } }}
131
+ placeholder="Paste the .aln contents here"
132
+ value={msaText}
133
+ onChange={event => {
134
+ setMsaText(event.target.value)
135
+ }}
136
+ />
137
+ <TextField2
138
+ variant="outlined"
139
+ label="Tree (optional)"
140
+ multiline
141
+ minRows={3}
142
+ maxRows={10}
143
+ fullWidth
144
+ slotProps={{ input: { className: classes.textAreaFont } }}
145
+ placeholder="Paste the .nh Newick tree here"
146
+ value={treeText}
147
+ onChange={event => {
148
+ setTreeText(event.target.value)
149
+ }}
150
+ />
151
+
152
+ <QueryRowSelector {...queryRow} />
153
+ </div>
154
+ </div>
70
155
  </LaunchPanelContent>
71
156
 
72
- <DialogActions>
73
- <Button
74
- color="primary"
75
- variant="contained"
76
- onClick={() => {
77
- handleClose()
78
- }}
79
- >
80
- Close
81
- </Button>
82
- </DialogActions>
157
+ <SubmitCancelActions
158
+ model={model}
159
+ submitDisabled={!selectedTranscript || !msaText.trim()}
160
+ onSubmit={() => {
161
+ try {
162
+ if (selectedTranscript) {
163
+ setLaunchViewError(undefined)
164
+ launchView({
165
+ newViewTitle: getGeneDisplayName(selectedTranscript),
166
+ view,
167
+ feature: selectedTranscript,
168
+ querySeqName: queryRow.querySeqName,
169
+ data: { msa: msaText, tree: treeText },
170
+ })
171
+ handleClose()
172
+ }
173
+ } catch (e) {
174
+ console.error(e)
175
+ setLaunchViewError(e)
176
+ }
177
+ }}
178
+ onCancel={handleClose}
179
+ />
83
180
  </>
84
181
  )
85
182
  })
@@ -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,
@@ -9,10 +9,17 @@ export const BASE_BLAST_URL = 'https://blast.ncbi.nlm.nih.gov/Blast.cgi'
9
9
  export const msaAlgorithms = ['clustalo', 'muscle', 'kalign', 'mafft'] as const
10
10
  export type MsaAlgorithm = (typeof msaAlgorithms)[number]
11
11
 
12
+ /**
13
+ * EBI rejects a submission naming a database outside its own list with a 400,
14
+ * so every value here has to appear in
15
+ * https://www.ebi.ac.uk/Tools/services/rest/ncbiblast/parameterdetails/database
16
+ * -- `uniprotkb_reference_proteomes` did not, and 3.0.0 shipped it as a dead
17
+ * menu entry.
18
+ */
12
19
  export const blastDatabaseOptions = [
13
20
  'uniprotkb_swissprot',
14
21
  'uniprotkb',
15
- 'uniprotkb_reference_proteomes',
22
+ 'pan_proteomes',
16
23
  'uniprotkb_trembl',
17
24
  ] as const
18
25
  export type BlastDatabase = (typeof blastDatabaseOptions)[number]
@@ -1,20 +1,16 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
3
  import { FileSelector } from '@jbrowse/core/ui'
4
- import {
5
- Alert,
6
- FormControl,
7
- FormControlLabel,
8
- Radio,
9
- RadioGroup,
10
- } from '@mui/material'
4
+ import { FormControl, FormControlLabel, Radio, RadioGroup } from '@mui/material'
11
5
  import { observer } from 'mobx-react'
12
6
  import { makeStyles } from 'tss-react/mui'
13
7
 
14
8
  import { launchView } from './launchView'
15
9
  import TextField2 from '../../../components/TextField2'
10
+ import { useQueryRowName } from '../../useQueryRowName'
16
11
  import { getGeneDisplayName, getLinearGenomeView } from '../../util'
17
12
  import LaunchPanelContent from '../LaunchPanelContent'
13
+ import QueryRowSelector from '../QueryRowSelector'
18
14
  import SubmitCancelActions from '../SubmitCancelActions'
19
15
  import TranscriptSelector from '../TranscriptSelector'
20
16
  import { useTranscriptSelection } from '../useTranscriptSelection'
@@ -38,12 +34,6 @@ const useStyles = makeStyles()({
38
34
  msaInput: {
39
35
  marginBottom: 20,
40
36
  },
41
- queryNameInput: {
42
- marginTop: 20,
43
- },
44
- warningAlert: {
45
- marginTop: 10,
46
- },
47
37
  })
48
38
 
49
39
  const ManualMSALoader = observer(function PreLoadedMSA2({
@@ -63,9 +53,9 @@ const ManualMSALoader = observer(function PreLoadedMSA2({
63
53
  const [treeText, setTreeText] = useState('')
64
54
  const [msaFileLocation, setMsaFileLocation] = useState<FileLocation>()
65
55
  const [treeFileLocation, setTreeFileLocation] = useState<FileLocation>()
66
- const [querySeqName, setQuerySeqName] = useState('')
67
56
  const transcriptSelection = useTranscriptSelection({ feature, view })
68
- const { selectedTranscript, error } = transcriptSelection
57
+ const { selectedTranscript, proteinSequence, error } = transcriptSelection
58
+ const queryRow = useQueryRowName(msaText, proteinSequence)
69
59
 
70
60
  const e = launchViewError ?? error
71
61
  return (
@@ -143,30 +133,11 @@ const ManualMSALoader = observer(function PreLoadedMSA2({
143
133
 
144
134
  <TranscriptSelector feature={feature} {...transcriptSelection} />
145
135
 
146
- <TextField2
147
- variant="outlined"
148
- name="MSA row name"
149
- fullWidth
150
- required
151
- className={classes.queryNameInput}
152
- placeholder="Row name in MSA that corresponds to the selected transcript"
153
- helperText="Required: Specify the name of the row in your MSA that should be aligned with the selected transcript"
154
- value={querySeqName}
155
- onChange={event => {
156
- setQuerySeqName(event.target.value)
157
- }}
158
- />
159
-
160
- {!querySeqName.trim() ? (
161
- <Alert severity="warning" className={classes.warningAlert}>
162
- Without specifying the MSA row name, clicking on the MSA will not
163
- navigate to the corresponding genome position, and hovering
164
- highlights will not work.
165
- </Alert>
166
- ) : null}
136
+ <QueryRowSelector {...queryRow} />
167
137
  </LaunchPanelContent>
168
138
 
169
139
  <SubmitCancelActions
140
+ model={model}
170
141
  submitDisabled={
171
142
  !selectedTranscript ||
172
143
  (inputMethod === 'file' && !msaFileLocation) ||
@@ -180,7 +151,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({
180
151
  newViewTitle: getGeneDisplayName(selectedTranscript),
181
152
  view,
182
153
  feature: selectedTranscript,
183
- querySeqName: querySeqName.trim(),
154
+ querySeqName: queryRow.querySeqName,
184
155
  ...(inputMethod === 'file'
185
156
  ? {
186
157
  msaFilehandle: msaFileLocation,