@jbrowse/app-core 5.0.0-beta.1 → 5.0.0-beta.3
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/esm/JBrowseConfig/RootConfiguration.d.ts +4 -0
- package/esm/JBrowseConfig/index.d.ts +4 -0
- package/esm/JBrowseModel/index.d.ts +4 -0
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +322 -0
- package/esm/JbApi/jbApi.js +867 -0
- package/esm/RootMenu/index.d.ts +2 -11
- package/esm/RootMenu/index.js +0 -11
- package/esm/SessionSpec/loadSessionSpec.js +145 -78
- package/esm/SessionSpec/types.d.ts +10 -12
- package/esm/WorkspaceLayout/model.d.ts +24 -4
- package/esm/WorkspaceLayout/model.js +79 -17
- package/esm/WorkspaceLayout/spec.d.ts +40 -10
- package/esm/WorkspaceLayout/spec.js +81 -8
- package/esm/index.d.ts +3 -1
- package/esm/index.js +3 -0
- package/esm/menus.d.ts +15 -29
- package/esm/menus.js +76 -92
- package/esm/ui/App/AppReadyMarker.d.ts +10 -1
- package/esm/ui/App/AppReadyMarker.js +27 -21
- package/esm/ui/App/ViewButtons.js +2 -2
- package/esm/ui/App/ViewLauncher.js +1 -1
- package/package.json +5 -5
package/esm/RootMenu/index.d.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import type { MenuAction
|
|
2
|
-
import type { MenuItem } from '@jbrowse/core/ui/
|
|
1
|
+
import type { MenuAction } from '../menus.ts';
|
|
2
|
+
import type { MenuItem } from '@jbrowse/core/ui/menuItems';
|
|
3
3
|
/**
|
|
4
4
|
* #stateModel RootAppMenuMixin
|
|
5
5
|
*/
|
|
6
6
|
export declare function RootAppMenuMixin(): import("@jbrowse/mobx-state-tree").IModelType<{}, {
|
|
7
7
|
mutableMenuActions: MenuAction[];
|
|
8
8
|
} & {
|
|
9
|
-
/**
|
|
10
|
-
* #action
|
|
11
|
-
* Replace the menu bar wholesale. Item contributions recorded before
|
|
12
|
-
* this one are dropped along with the menus they targeted, so a plugin
|
|
13
|
-
* adding to the existing bar wants `appendToMenu` instead.
|
|
14
|
-
*
|
|
15
|
-
* @param newMenus - The complete new set of top-level menus.
|
|
16
|
-
*/
|
|
17
|
-
setMenus(newMenus: MenuDefinition[]): void;
|
|
18
9
|
/**
|
|
19
10
|
* #action
|
|
20
11
|
* Add a top-level menu, if the app bar does not already have one with
|
package/esm/RootMenu/index.js
CHANGED
|
@@ -14,17 +14,6 @@ export function RootAppMenuMixin() {
|
|
|
14
14
|
self.mutableMenuActions = [...self.mutableMenuActions, action];
|
|
15
15
|
}
|
|
16
16
|
return {
|
|
17
|
-
/**
|
|
18
|
-
* #action
|
|
19
|
-
* Replace the menu bar wholesale. Item contributions recorded before
|
|
20
|
-
* this one are dropped along with the menus they targeted, so a plugin
|
|
21
|
-
* adding to the existing bar wants `appendToMenu` instead.
|
|
22
|
-
*
|
|
23
|
-
* @param newMenus - The complete new set of top-level menus.
|
|
24
|
-
*/
|
|
25
|
-
setMenus(newMenus) {
|
|
26
|
-
pushAction({ type: 'setMenus', newMenus });
|
|
27
|
-
},
|
|
28
17
|
/**
|
|
29
18
|
* #action
|
|
30
19
|
* Add a top-level menu, if the app bar does not already have one with
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { isSessionWithAddAssembly, isSessionWithAddSessionTrack, } from '@jbrowse/core/util';
|
|
2
|
+
import { legacyInitMessage, unknownKeysMessage, } from '@jbrowse/core/util/withLaunchInput';
|
|
2
3
|
import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
3
4
|
import { when } from 'mobx';
|
|
4
5
|
function isSessionWithWorkspaceLayout(session) {
|
|
5
|
-
return
|
|
6
|
-
'setUseWorkspaces' in session &&
|
|
7
|
-
'orderViews' in session);
|
|
6
|
+
return 'layoutViews' in session;
|
|
8
7
|
}
|
|
9
8
|
function isSessionWithSpecConnections(session) {
|
|
10
9
|
return ('addConnectionConf' in session &&
|
|
@@ -57,37 +56,75 @@ async function whenConnectionsSettle(session) {
|
|
|
57
56
|
await when(() => gone() || session.connectionInstances.every(conn => !conn.loading));
|
|
58
57
|
return !gone();
|
|
59
58
|
}
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
59
|
+
// Resolve a spec layout's view indexes to ids before `applyLayoutSpec` counts
|
|
60
|
+
// them into `session.views`, which is a different list: a spec index names a
|
|
61
|
+
// spec entry, and `createdViewIds[i]` is EVERY view the i-th entry created, in
|
|
62
|
+
// creation order. A connected ProteinView creates its genome view and then
|
|
63
|
+
// itself, and the index names both — stacked in one cell — where recording one
|
|
64
|
+
// id per entry left the structure unaddressable, in no cell, and the panel
|
|
65
|
+
// naming it empty. A string is a view id and passes through; a reference that
|
|
66
|
+
// resolves to nothing is dropped here after `unresolvedLayoutRefs` reported it.
|
|
67
|
+
function convertLayoutNode(node, createdViewIds, knownIds, pinnedIds) {
|
|
68
|
+
const { views, children, ...rest } = node;
|
|
69
|
+
return {
|
|
70
|
+
...rest,
|
|
71
|
+
...(views === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: {
|
|
74
|
+
views: Array.isArray(views)
|
|
75
|
+
? views.flatMap(ref => typeof ref === 'number'
|
|
76
|
+
? (createdViewIds[ref] ?? []).filter(id => !pinnedIds.has(id))
|
|
77
|
+
: knownIds.has(ref)
|
|
78
|
+
? [ref]
|
|
79
|
+
: [])
|
|
80
|
+
: views,
|
|
81
|
+
}),
|
|
82
|
+
...(children === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: {
|
|
85
|
+
children: Array.isArray(children)
|
|
86
|
+
? children.map(child => convertLayoutNode(child, createdViewIds, knownIds, pinnedIds))
|
|
87
|
+
: children,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// An index names every view its entry created, so the entry that opens a genome
|
|
92
|
+
// view beside its own puts BOTH in the cell naming it — which is the point, and
|
|
93
|
+
// is wrong the moment another cell names one of them by id. `{views:[0]}` next
|
|
94
|
+
// to `{views:['structure']}` is the obvious spelling of "genome left, structure
|
|
95
|
+
// right", and expanding the index blind seats the structure in both cells: two
|
|
96
|
+
// React trees and two GPU contexts for one model, which `resolveLayoutSpec` now
|
|
97
|
+
// refuses outright. An id stated by hand is the more specific statement, so it
|
|
98
|
+
// wins its view and the index gives it up.
|
|
99
|
+
function pinnedLayoutIds(node, knownIds) {
|
|
100
|
+
return [
|
|
101
|
+
...(Array.isArray(node.views)
|
|
102
|
+
? node.views.filter((ref) => typeof ref === 'string' && knownIds.has(ref))
|
|
103
|
+
: []),
|
|
104
|
+
...(Array.isArray(node.children)
|
|
105
|
+
? node.children.flatMap(child => pinnedLayoutIds(child, knownIds))
|
|
106
|
+
: []),
|
|
107
|
+
];
|
|
82
108
|
}
|
|
83
109
|
// A layout index is a position in the spec's `views` array, so one past the end
|
|
84
110
|
// (or negative) is an authoring slip that would otherwise silently drop that
|
|
85
|
-
// view from the layout, or, if every
|
|
86
|
-
// panel with no clue why.
|
|
87
|
-
|
|
111
|
+
// view from the layout, or, if every reference in a panel is bad, leave an
|
|
112
|
+
// empty panel with no clue why. An id names a view pinned with the spec's `id`
|
|
113
|
+
// key, so one nothing created is the same slip.
|
|
114
|
+
function unresolvedLayoutRefs(node, viewCount, knownIds) {
|
|
115
|
+
// Array-guarded rather than `?.`: a hand-written `views: 0` is non-nullish,
|
|
116
|
+
// so `?.` does not short-circuit and `(0).filter` threw a raw TypeError out
|
|
117
|
+
// of this validator — past `resolveLayoutSpec`, which has a real message for
|
|
118
|
+
// that exact slip and never got to say it.
|
|
88
119
|
return [
|
|
89
|
-
...(node.views
|
|
90
|
-
|
|
120
|
+
...(Array.isArray(node.views)
|
|
121
|
+
? node.views.filter(ref => typeof ref === 'number'
|
|
122
|
+
? ref < 0 || ref >= viewCount
|
|
123
|
+
: !knownIds.has(ref))
|
|
124
|
+
: []),
|
|
125
|
+
...(Array.isArray(node.children)
|
|
126
|
+
? node.children.flatMap(child => unresolvedLayoutRefs(child, viewCount, knownIds))
|
|
127
|
+
: []),
|
|
91
128
|
];
|
|
92
129
|
}
|
|
93
130
|
// A `tabs` node shares one cell between its children rather than dividing the
|
|
@@ -103,9 +140,9 @@ function outOfRangeLayoutIndices(node, viewCount) {
|
|
|
103
140
|
// nested or partial sizes had been ignored while the layout honoured them —
|
|
104
141
|
// which is also what website/docs/urlparams.md promises.
|
|
105
142
|
function unsizeableLayoutNodes(layout) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
return
|
|
143
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
144
|
+
const sizedTabsChild = layout.direction === 'tabs' && children.some(c => c.size !== undefined);
|
|
145
|
+
return sizedTabsChild || children.some(unsizeableLayoutNodes);
|
|
109
146
|
}
|
|
110
147
|
// The other statement a `tabs` node cannot take literally, and the one that
|
|
111
148
|
// used to fail silently rather than partially: a tab holds a FLAT stack of
|
|
@@ -115,9 +152,9 @@ function unsizeableLayoutNodes(layout) {
|
|
|
115
152
|
// to be dropped from the layout, after which homing swept them into whichever
|
|
116
153
|
// tab happened to be showing, so the arrangement was wrong with nothing said.
|
|
117
154
|
function flattenedTabsContainers(layout) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
return
|
|
155
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
156
|
+
const nestedTabsChild = layout.direction === 'tabs' && children.some(c => c.children !== undefined);
|
|
157
|
+
return nestedTabsChild || children.some(flattenedTabsContainers);
|
|
121
158
|
}
|
|
122
159
|
// use extension point named e.g. LaunchView-LinearGenomeView to initialize an
|
|
123
160
|
// LGV session
|
|
@@ -202,27 +239,47 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
202
239
|
if (noLauncher.length) {
|
|
203
240
|
session?.notifyError(`View type(s) ${noLauncher.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`);
|
|
204
241
|
}
|
|
205
|
-
//
|
|
206
|
-
//
|
|
242
|
+
// v4 nested a view's settings under `init`. A spec never becomes a
|
|
243
|
+
// snapshot, so `withLaunchInput`'s unwrap never runs on one and this
|
|
244
|
+
// surface does it itself, in the same order: the flat spelling wins.
|
|
245
|
+
const specViews = views.map(({ init, ...view }) => {
|
|
246
|
+
if (init) {
|
|
247
|
+
console.warn(legacyInitMessage(view.type));
|
|
248
|
+
}
|
|
249
|
+
return init ? { ...init, ...view } : view;
|
|
250
|
+
});
|
|
251
|
+
// The same classification a view snapshot gets, run here because a spec
|
|
252
|
+
// never becomes one: `LaunchView-<type>` takes these keys as arguments, so
|
|
253
|
+
// `withLaunchInput`'s partition never sees them and nothing names a typo.
|
|
254
|
+
// `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the
|
|
255
|
+
// launcher's downstream "No assembly provided", on the one surface written
|
|
256
|
+
// by hand with no compiler and no editor behind it.
|
|
257
|
+
//
|
|
258
|
+
// An ERROR rather than the snapshot path's warning: this surface's
|
|
259
|
+
// misplaced keys already report as errors, and the launcher's own failure
|
|
260
|
+
// lands as an error a line later — a warning under it reads as the lesser
|
|
261
|
+
// of the two, when it is the cause.
|
|
207
262
|
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// - a config/defaultSession view is MST *state*, so keys that need resolving
|
|
212
|
-
// on load have nowhere to live but the view's own `init` property
|
|
263
|
+
// A view type that registers no launch keys classifies nothing: its
|
|
264
|
+
// launcher's vocabulary is undeclared, so every argument would read as a
|
|
265
|
+
// typo.
|
|
213
266
|
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
267
|
+
// A lazily registered view type declares its properties on a state model
|
|
268
|
+
// that is not loaded yet, so it is loaded first — the launch below would
|
|
269
|
+
// have loaded it anyway.
|
|
270
|
+
await Promise.all(specViews
|
|
271
|
+
.filter(({ type }) => viewTypes.has(type))
|
|
272
|
+
.map(({ type }) => pluginManager.getViewType(type).loadStateModel()));
|
|
273
|
+
for (const { type, ...view } of specViews) {
|
|
274
|
+
const accepted = viewTypes.has(type)
|
|
275
|
+
? pluginManager.getViewType(type).acceptedKeys
|
|
276
|
+
: undefined;
|
|
277
|
+
const unknown = accepted
|
|
278
|
+
? Object.keys(view).filter(key => !accepted.includes(key))
|
|
279
|
+
: [];
|
|
280
|
+
if (unknown.length) {
|
|
281
|
+
session?.notifyError(unknownKeysMessage(type, unknown));
|
|
282
|
+
}
|
|
226
283
|
}
|
|
227
284
|
// Launch sequentially and record the id each spec view created, so the
|
|
228
285
|
// layout below can map its indices to real views. Reading session.views
|
|
@@ -230,13 +287,12 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
230
287
|
// synchronously and in order; capturing the delta per launch instead is
|
|
231
288
|
// correct even if a handler awaits or adds an auxiliary view, and lets a
|
|
232
289
|
// later spec view (e.g. a connected MsaView) reference an earlier one that
|
|
233
|
-
// now already exists. `type` is the dispatch key, not
|
|
234
|
-
//
|
|
235
|
-
//
|
|
290
|
+
// now already exists. `type` is the dispatch key, not a setting: forwarding
|
|
291
|
+
// it would land in the view's launch blob and trip the spurious
|
|
292
|
+
// "ignored unknown key(s): type" warning meant to catch typos.
|
|
236
293
|
// `displayName` is applied here rather than forwarded because it is a base
|
|
237
294
|
// view prop every view type has, so one path covers all of them (including
|
|
238
|
-
// plugin-provided types whose launcher never heard of it).
|
|
239
|
-
// NOT unwrapped the same way — see the diagnostic above for why.
|
|
295
|
+
// plugin-provided types whose launcher never heard of it).
|
|
240
296
|
// Let any connection this spec registered finish before the views that
|
|
241
297
|
// reference what it supplies are launched (see whenConnectionsSettle). Gated
|
|
242
298
|
// on there being views: with none, nothing is waiting on the connection, and
|
|
@@ -251,7 +307,7 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
251
307
|
return;
|
|
252
308
|
}
|
|
253
309
|
const createdViewIds = [];
|
|
254
|
-
for (const { type, displayName, ...view } of
|
|
310
|
+
for (const { type, displayName, ...view } of specViews) {
|
|
255
311
|
const before = new Set(session?.views.map(v => v.id));
|
|
256
312
|
// Strict so a launch handler that throws (missing/invalid assembly,
|
|
257
313
|
// unresolved track, ...) surfaces as a snackbar instead of being swallowed
|
|
@@ -268,17 +324,27 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
268
324
|
console.error(e);
|
|
269
325
|
session?.notifyError(`Failed to launch ${type} view: ${e}`, e);
|
|
270
326
|
}
|
|
271
|
-
const created = session?.views.
|
|
272
|
-
|
|
273
|
-
|
|
327
|
+
const created = session?.views.filter(v => !before.has(v.id)) ?? [];
|
|
328
|
+
// The entry's own view, when its launcher created others beside it: a
|
|
329
|
+
// connected ProteinView's name went to the genome view it opened first,
|
|
330
|
+
// and the structure came up "Untitled view".
|
|
331
|
+
const named = created.find(v => v.type === type) ?? created[0];
|
|
332
|
+
if (named && displayName) {
|
|
333
|
+
named.setDisplayName(displayName);
|
|
274
334
|
}
|
|
275
|
-
createdViewIds.push(created
|
|
335
|
+
createdViewIds.push(created.map(v => v.id));
|
|
276
336
|
}
|
|
277
337
|
if (layout && session) {
|
|
278
|
-
const
|
|
338
|
+
const knownIds = new Set(createdViewIds.flat());
|
|
339
|
+
const bad = unresolvedLayoutRefs(layout, views.length, knownIds);
|
|
340
|
+
const badIndices = bad.filter(ref => typeof ref === 'number');
|
|
341
|
+
const badIds = bad.filter(ref => typeof ref === 'string');
|
|
279
342
|
if (badIndices.length) {
|
|
280
343
|
session.notifyError(`Session spec layout references view index ${badIndices.join(', ')}, but the spec has ${views.length} view(s).`);
|
|
281
344
|
}
|
|
345
|
+
if (badIds.length) {
|
|
346
|
+
session.notifyError(`Session spec layout references view id ${badIds.map(id => `"${id}"`).join(', ')}, but no view in the spec has that id (a view's "id" key pins it).`);
|
|
347
|
+
}
|
|
282
348
|
if (unsizeableLayoutNodes(layout)) {
|
|
283
349
|
session.notify('Session spec layout: a "tabs" node shares one cell between its children rather than dividing the space, so the "size" on them was ignored. The rest of the layout is unaffected.', 'info');
|
|
284
350
|
}
|
|
@@ -286,18 +352,19 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
286
352
|
session.notify('Session spec layout: a "tabs" node gives each child one tab, and a tab holds a flat stack of views — so a container inside one was flattened into a single tab rather than splitting it. The rest of the layout is unaffected.', 'info');
|
|
287
353
|
}
|
|
288
354
|
if (isSessionWithWorkspaceLayout(session)) {
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
// in
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
355
|
+
// Its own try/catch, the same reasoning as the per-view one above: the
|
|
356
|
+
// resolver throws for a layout it will not arrange, and this is the
|
|
357
|
+
// LAST thing the spec does — so an unarrangeable layout used to take
|
|
358
|
+
// the whole load's catch and report itself as the session's error. The
|
|
359
|
+
// layout is one statement in the spec; losing it should cost the spec
|
|
360
|
+
// its layout and nothing else.
|
|
361
|
+
try {
|
|
362
|
+
session.layoutViews(convertLayoutNode(layout, createdViewIds, knownIds, new Set(pinnedLayoutIds(layout, knownIds))));
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
console.error(e);
|
|
366
|
+
session.notifyError(`Session spec layout: ${e}`, e);
|
|
367
|
+
}
|
|
301
368
|
}
|
|
302
369
|
else {
|
|
303
370
|
session.notifyError('Session spec has a "layout", but this application does not support workspace layouts');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LayoutSpecNode } from '../WorkspaceLayout/spec.ts';
|
|
1
2
|
import type { TrackInit } from '@jbrowse/core/util/tracks';
|
|
2
3
|
export interface ViewSpec {
|
|
3
4
|
type: string;
|
|
@@ -6,15 +7,17 @@ export interface ViewSpec {
|
|
|
6
7
|
tracks?: TrackInit[];
|
|
7
8
|
assembly?: string;
|
|
8
9
|
loc?: string;
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated v4's nesting. Write every setting directly on the view object;
|
|
12
|
+
* this is unwrapped on the way in and warns.
|
|
13
|
+
*/
|
|
14
|
+
init?: Record<string, unknown>;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
17
|
+
* A spec's `layout`: the one layout shape, with a leaf's `views` counting into
|
|
18
|
+
* the spec's own `views` array (an index names every view that entry created)
|
|
19
|
+
* or naming a view id the spec pinned with `id`.
|
|
12
20
|
*
|
|
13
|
-
* A LayoutNode is either:
|
|
14
|
-
* - A panel (has `views` array) - displays views stacked vertically
|
|
15
|
-
* - A container (has `children` array) - arranges children horizontally or vertically
|
|
16
|
-
*
|
|
17
|
-
* Example - horizontal split with custom sizes:
|
|
18
21
|
* ```json
|
|
19
22
|
* {
|
|
20
23
|
* "direction": "horizontal",
|
|
@@ -25,9 +28,4 @@ export interface ViewSpec {
|
|
|
25
28
|
* }
|
|
26
29
|
* ```
|
|
27
30
|
*/
|
|
28
|
-
export
|
|
29
|
-
views?: number[];
|
|
30
|
-
direction?: 'horizontal' | 'vertical' | 'tabs';
|
|
31
|
-
children?: LayoutNode[];
|
|
32
|
-
size?: number;
|
|
33
|
-
}
|
|
31
|
+
export type LayoutNode = LayoutSpecNode;
|
|
@@ -182,6 +182,14 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
|
|
|
182
182
|
/**
|
|
183
183
|
* Arrange the workspace as a spec states.
|
|
184
184
|
*
|
|
185
|
+
* The spec is the same shape a session spec's `layout` takes: a leaf's
|
|
186
|
+
* `views` names views by index into `session.views` or by id, so
|
|
187
|
+
* `{ direction: 'horizontal', children: [{ views: [0] }, { views: [1] }] }`
|
|
188
|
+
* means here what it means in a link. A leaf spelled any other way, an
|
|
189
|
+
* index past the end or an id no view has throws rather than arranging
|
|
190
|
+
* nothing — an untyped caller used to collapse the workspace into one
|
|
191
|
+
* blank tab that way.
|
|
192
|
+
*
|
|
185
193
|
* There is no `init` property and no standing request: the spec is
|
|
186
194
|
* converted and *becomes* the layout, here and now. `init` existed only
|
|
187
195
|
* because dockview had to be told, could not be told before it mounted,
|
|
@@ -192,16 +200,16 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
|
|
|
192
200
|
/**
|
|
193
201
|
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
194
202
|
*
|
|
195
|
-
* `allViewIds` is EVERY view in the session,
|
|
196
|
-
*
|
|
203
|
+
* `allViewIds` is EVERY view in the session, defaulting to the host's
|
|
204
|
+
* own list — homing drops any view the list does not name, so the
|
|
197
205
|
* `[viewId]` default this used to carry unhomed all the others.
|
|
198
206
|
*/
|
|
199
|
-
moveViewToNewTab(viewId: string, allViewIds
|
|
207
|
+
moveViewToNewTab(viewId: string, allViewIds?: string[]): string | undefined;
|
|
200
208
|
/**
|
|
201
209
|
* ViewMenu's "move to split view": the view leaves for a new cell.
|
|
202
210
|
* `allViewIds` is every view in the session — see `moveViewToNewTab`.
|
|
203
211
|
*/
|
|
204
|
-
moveViewToSplitRight(viewId: string, allViewIds
|
|
212
|
+
moveViewToSplitRight(viewId: string, allViewIds?: string[]): string | undefined;
|
|
205
213
|
homeUnassignedViews(viewIds: string[]): void;
|
|
206
214
|
} & {
|
|
207
215
|
/**
|
|
@@ -233,6 +241,18 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
|
|
|
233
241
|
* session spec's layout there is nothing for `orderViews` to apply.
|
|
234
242
|
*/
|
|
235
243
|
tileViews(mode: TileMode, allViewIds: string[]): void;
|
|
244
|
+
/**
|
|
245
|
+
* #action
|
|
246
|
+
* Arrange the session's views into panels: `applyLayoutSpec` plus the
|
|
247
|
+
* two things it does not do on its own, each of which fails silently
|
|
248
|
+
* without it — turning workspaces mode on for this session (a layout
|
|
249
|
+
* renders nowhere else) and ordering `session.views` to the spec's
|
|
250
|
+
* top-to-bottom statement, which is where a tab reads its order from.
|
|
251
|
+
* Leaves take view ids or indexes into `session.views`; a layout that
|
|
252
|
+
* seats no view throws rather than leaving a blank tab. What a session
|
|
253
|
+
* spec's `layout` and an agent's live re-layout both call.
|
|
254
|
+
*/
|
|
255
|
+
layoutViews(spec: LayoutSpecNode): string[];
|
|
236
256
|
}, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
|
|
237
257
|
export type WorkspaceLayoutMixinType = ReturnType<typeof WorkspaceLayoutMixin>;
|
|
238
258
|
export interface WorkspaceLayout extends Instance<WorkspaceLayoutMixinType> {
|
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import { createElementId } from '@jbrowse/core/util/types/mst';
|
|
2
2
|
import { cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
|
|
3
|
-
import { specForPendingMove, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
|
|
3
|
+
import { resolveLayoutSpec, specForPendingMove, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
|
|
4
4
|
import { activeTabIn, addTab, addViewToTab, findTab, homeViews, moveTabToPanel, normalize, panelContainingView, panels, pruneEmptyPanel, pruneEmptyTabIn, removePanel, removeTab, removeView, renameTab, setActiveTab, setSizes, splitPanel, tabContainingView, tabs, } from './tree.js';
|
|
5
|
+
function sessionViewIds(self) {
|
|
6
|
+
return self.views?.map(v => v.id);
|
|
7
|
+
}
|
|
8
|
+
function layoutHost(self) {
|
|
9
|
+
return self;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A caller-supplied view list narrowed to the views the session actually has.
|
|
13
|
+
*
|
|
14
|
+
* The list is the caller's snapshot, and `applyLayoutSpec` refuses an id no
|
|
15
|
+
* view has — so a view added after the list was built, or removed between
|
|
16
|
+
* building it and the call, would throw out of a sugar action no plugin wraps.
|
|
17
|
+
* The arrangement a stale name asks for is still the one it means for every
|
|
18
|
+
* view that IS there, so the name is dropped rather than the action. A repeat
|
|
19
|
+
* is dropped for the same reason: it would seat one view in two cells, which
|
|
20
|
+
* the resolver also refuses.
|
|
21
|
+
*/
|
|
22
|
+
function liveViewIds(self, allViewIds) {
|
|
23
|
+
const live = sessionViewIds(self);
|
|
24
|
+
return [
|
|
25
|
+
...new Set(live ? allViewIds.filter(id => live.includes(id)) : allViewIds),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
5
28
|
/**
|
|
6
29
|
* #stateModel WorkspaceLayoutMixin
|
|
7
30
|
*
|
|
@@ -210,6 +233,13 @@ export function WorkspaceLayoutMixin() {
|
|
|
210
233
|
* Returns where the view came from, or `undefined` if it is not in the
|
|
211
234
|
* session, in which case nothing is applied.
|
|
212
235
|
*/
|
|
236
|
+
function everyViewId(action, allViewIds) {
|
|
237
|
+
const ids = allViewIds ?? sessionViewIds(self);
|
|
238
|
+
if (!ids) {
|
|
239
|
+
throw new Error(`${action}(viewId, allViewIds): allViewIds is every view id in the session, and this session has no view list to default it from`);
|
|
240
|
+
}
|
|
241
|
+
return ids;
|
|
242
|
+
}
|
|
213
243
|
function rehomeView(viewId, allViewIds, place) {
|
|
214
244
|
const tree = home(self.tree, allViewIds);
|
|
215
245
|
const from = tabContainingView(tree, viewId);
|
|
@@ -400,6 +430,14 @@ export function WorkspaceLayoutMixin() {
|
|
|
400
430
|
/**
|
|
401
431
|
* Arrange the workspace as a spec states.
|
|
402
432
|
*
|
|
433
|
+
* The spec is the same shape a session spec's `layout` takes: a leaf's
|
|
434
|
+
* `views` names views by index into `session.views` or by id, so
|
|
435
|
+
* `{ direction: 'horizontal', children: [{ views: [0] }, { views: [1] }] }`
|
|
436
|
+
* means here what it means in a link. A leaf spelled any other way, an
|
|
437
|
+
* index past the end or an id no view has throws rather than arranging
|
|
438
|
+
* nothing — an untyped caller used to collapse the workspace into one
|
|
439
|
+
* blank tab that way.
|
|
440
|
+
*
|
|
403
441
|
* There is no `init` property and no standing request: the spec is
|
|
404
442
|
* converted and *becomes* the layout, here and now. `init` existed only
|
|
405
443
|
* because dockview had to be told, could not be told before it mounted,
|
|
@@ -407,20 +445,21 @@ export function WorkspaceLayoutMixin() {
|
|
|
407
445
|
* from the layout living somewhere this action could not reach.
|
|
408
446
|
*/
|
|
409
447
|
applyLayoutSpec(spec) {
|
|
410
|
-
|
|
448
|
+
const resolved = resolveLayoutSpec(spec, sessionViewIds(self));
|
|
449
|
+
apply(treeFromSpec(resolved, nextId));
|
|
411
450
|
self.activePanelId = panels(self.tree)[0]?.id;
|
|
412
|
-
return viewIdsInSpec(
|
|
451
|
+
return viewIdsInSpec(resolved);
|
|
413
452
|
},
|
|
414
453
|
/**
|
|
415
454
|
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
416
455
|
*
|
|
417
|
-
* `allViewIds` is EVERY view in the session,
|
|
418
|
-
*
|
|
456
|
+
* `allViewIds` is EVERY view in the session, defaulting to the host's
|
|
457
|
+
* own list — homing drops any view the list does not name, so the
|
|
419
458
|
* `[viewId]` default this used to carry unhomed all the others.
|
|
420
459
|
*/
|
|
421
460
|
moveViewToNewTab(viewId, allViewIds) {
|
|
422
461
|
const tab = { id: nextId('tab'), viewIds: [viewId] };
|
|
423
|
-
const from = rehomeView(viewId, allViewIds, (tree, at) => addTab(tree, at.panel.id, tab));
|
|
462
|
+
const from = rehomeView(viewId, everyViewId('moveViewToNewTab', allViewIds), (tree, at) => addTab(tree, at.panel.id, tab));
|
|
424
463
|
if (!from) {
|
|
425
464
|
return undefined;
|
|
426
465
|
}
|
|
@@ -439,7 +478,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
439
478
|
tabs: [{ id: tabId, viewIds: [viewId] }],
|
|
440
479
|
activeTabId: tabId,
|
|
441
480
|
};
|
|
442
|
-
const from = rehomeView(viewId, allViewIds, (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
|
|
481
|
+
const from = rehomeView(viewId, everyViewId('moveViewToSplitRight', allViewIds), (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
|
|
443
482
|
if (!from) {
|
|
444
483
|
return undefined;
|
|
445
484
|
}
|
|
@@ -480,21 +519,20 @@ export function WorkspaceLayoutMixin() {
|
|
|
480
519
|
* is spelled.
|
|
481
520
|
*/
|
|
482
521
|
setPendingMove(move, allViewIds) {
|
|
483
|
-
// The session's whole set of views, for the one entry point that cannot
|
|
484
|
-
// be handed it. Every other one takes it as an argument on purpose —
|
|
485
|
-
// this mixin owns the tree and knows nothing else — and this would too,
|
|
486
|
-
// if its caller were ours. Duck-typed because `views` is
|
|
487
|
-
// MultipleViewsSessionMixin's and composition is the only thing that
|
|
488
|
-
// puts the two together.
|
|
489
|
-
const { views } = self;
|
|
490
522
|
// Not `?? []`: homing drops every view the list does not name, so an
|
|
491
523
|
// empty one would answer "put this view beside nothing" and unhome the
|
|
492
524
|
// rest. Nothing to say is nothing to do.
|
|
493
|
-
const ids = allViewIds ??
|
|
525
|
+
const ids = allViewIds ?? sessionViewIds(self);
|
|
494
526
|
if (!move || !ids) {
|
|
495
527
|
return;
|
|
496
528
|
}
|
|
497
|
-
self
|
|
529
|
+
const known = liveViewIds(self, ids);
|
|
530
|
+
// Nothing to move: the view named is not in the session, so there is
|
|
531
|
+
// no arrangement to state about it.
|
|
532
|
+
if (sessionViewIds(self) && !known.includes(move.viewId)) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
self.applyLayoutSpec(specForPendingMove(move, known));
|
|
498
536
|
// Show where the view went. A spec states an arrangement and not a
|
|
499
537
|
// selection, so `treeFromSpec` shows each cell's FIRST tab — and
|
|
500
538
|
// `newTab` puts the moved view in a tab beside the others, which makes
|
|
@@ -518,7 +556,31 @@ export function WorkspaceLayoutMixin() {
|
|
|
518
556
|
* session spec's layout there is nothing for `orderViews` to apply.
|
|
519
557
|
*/
|
|
520
558
|
tileViews(mode, allViewIds) {
|
|
521
|
-
self.applyLayoutSpec(tileLayoutSpec(allViewIds, mode));
|
|
559
|
+
self.applyLayoutSpec(tileLayoutSpec(liveViewIds(self, allViewIds), mode));
|
|
560
|
+
},
|
|
561
|
+
/**
|
|
562
|
+
* #action
|
|
563
|
+
* Arrange the session's views into panels: `applyLayoutSpec` plus the
|
|
564
|
+
* two things it does not do on its own, each of which fails silently
|
|
565
|
+
* without it — turning workspaces mode on for this session (a layout
|
|
566
|
+
* renders nowhere else) and ordering `session.views` to the spec's
|
|
567
|
+
* top-to-bottom statement, which is where a tab reads its order from.
|
|
568
|
+
* Leaves take view ids or indexes into `session.views`; a layout that
|
|
569
|
+
* seats no view throws rather than leaving a blank tab. What a session
|
|
570
|
+
* spec's `layout` and an agent's live re-layout both call.
|
|
571
|
+
*/
|
|
572
|
+
layoutViews(spec) {
|
|
573
|
+
const host = layoutHost(self);
|
|
574
|
+
if (!host.setUseWorkspaces || !host.orderViews) {
|
|
575
|
+
throw new Error('This session has no view list to arrange: layoutViews needs a host composed with MultipleViewsSessionMixin');
|
|
576
|
+
}
|
|
577
|
+
host.setUseWorkspaces(true);
|
|
578
|
+
const ids = self.applyLayoutSpec(spec);
|
|
579
|
+
if (ids.length === 0) {
|
|
580
|
+
throw new Error('The layout seats no views: a leaf names its views with "views" (view ids, or indexes into session.views) and a container nests "children"');
|
|
581
|
+
}
|
|
582
|
+
host.orderViews(ids);
|
|
583
|
+
return ids;
|
|
522
584
|
},
|
|
523
585
|
})));
|
|
524
586
|
}
|