jbrowse-plugin-msaview 3.1.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.
- 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 +1 -1
- 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/index.js +4 -1
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +28 -28
- 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/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 +1 -1
- 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/index.ts +4 -1
- package/src/utils/launchMsaView.ts +30 -0
- package/src/utils/workspaces.test.ts +132 -0
- package/src/utils/workspaces.ts +146 -0
- 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.
|
|
1
|
+
export declare const version = "3.2.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.
|
|
1
|
+
export const version = '3.2.0';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
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",
|
|
@@ -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(),
|
|
@@ -98,9 +98,9 @@ const OrthologPanel = observer(function ({
|
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
100
|
<TranscriptSelector feature={feature} {...transcriptSelection} />
|
|
101
|
-
|
|
102
101
|
</LaunchPanelContent>
|
|
103
102
|
<SubmitCancelActions
|
|
103
|
+
model={model}
|
|
104
104
|
submitDisabled={!proteinSequence || !rowCountValid}
|
|
105
105
|
onSubmit={() => {
|
|
106
106
|
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 { 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
|
}
|