@jbrowse/app-core 5.0.0-beta.1 → 5.0.0-beta.2
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 +325 -0
- package/esm/JbApi/jbApi.js +748 -0
- package/esm/RootMenu/index.d.ts +2 -11
- package/esm/RootMenu/index.js +0 -11
- package/esm/SessionSpec/loadSessionSpec.js +145 -64
- package/esm/SessionSpec/types.d.ts +10 -12
- package/esm/WorkspaceLayout/model.d.ts +12 -4
- package/esm/WorkspaceLayout/model.js +52 -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 +21 -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,4 +1,5 @@
|
|
|
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) {
|
|
@@ -57,37 +58,75 @@ async function whenConnectionsSettle(session) {
|
|
|
57
58
|
await when(() => gone() || session.connectionInstances.every(conn => !conn.loading));
|
|
58
59
|
return !gone();
|
|
59
60
|
}
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
// Resolve a spec layout's view indexes to ids before `applyLayoutSpec` counts
|
|
62
|
+
// them into `session.views`, which is a different list: a spec index names a
|
|
63
|
+
// spec entry, and `createdViewIds[i]` is EVERY view the i-th entry created, in
|
|
64
|
+
// creation order. A connected ProteinView creates its genome view and then
|
|
65
|
+
// itself, and the index names both — stacked in one cell — where recording one
|
|
66
|
+
// id per entry left the structure unaddressable, in no cell, and the panel
|
|
67
|
+
// naming it empty. A string is a view id and passes through; a reference that
|
|
68
|
+
// resolves to nothing is dropped here after `unresolvedLayoutRefs` reported it.
|
|
69
|
+
function convertLayoutNode(node, createdViewIds, knownIds, pinnedIds) {
|
|
70
|
+
const { views, children, ...rest } = node;
|
|
71
|
+
return {
|
|
72
|
+
...rest,
|
|
73
|
+
...(views === undefined
|
|
74
|
+
? {}
|
|
75
|
+
: {
|
|
76
|
+
views: Array.isArray(views)
|
|
77
|
+
? views.flatMap(ref => typeof ref === 'number'
|
|
78
|
+
? (createdViewIds[ref] ?? []).filter(id => !pinnedIds.has(id))
|
|
79
|
+
: knownIds.has(ref)
|
|
80
|
+
? [ref]
|
|
81
|
+
: [])
|
|
82
|
+
: views,
|
|
83
|
+
}),
|
|
84
|
+
...(children === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: {
|
|
87
|
+
children: Array.isArray(children)
|
|
88
|
+
? children.map(child => convertLayoutNode(child, createdViewIds, knownIds, pinnedIds))
|
|
89
|
+
: children,
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// An index names every view its entry created, so the entry that opens a genome
|
|
94
|
+
// view beside its own puts BOTH in the cell naming it — which is the point, and
|
|
95
|
+
// is wrong the moment another cell names one of them by id. `{views:[0]}` next
|
|
96
|
+
// to `{views:['structure']}` is the obvious spelling of "genome left, structure
|
|
97
|
+
// right", and expanding the index blind seats the structure in both cells: two
|
|
98
|
+
// React trees and two GPU contexts for one model, which `resolveLayoutSpec` now
|
|
99
|
+
// refuses outright. An id stated by hand is the more specific statement, so it
|
|
100
|
+
// wins its view and the index gives it up.
|
|
101
|
+
function pinnedLayoutIds(node, knownIds) {
|
|
102
|
+
return [
|
|
103
|
+
...(Array.isArray(node.views)
|
|
104
|
+
? node.views.filter((ref) => typeof ref === 'string' && knownIds.has(ref))
|
|
105
|
+
: []),
|
|
106
|
+
...(Array.isArray(node.children)
|
|
107
|
+
? node.children.flatMap(child => pinnedLayoutIds(child, knownIds))
|
|
108
|
+
: []),
|
|
109
|
+
];
|
|
82
110
|
}
|
|
83
111
|
// A layout index is a position in the spec's `views` array, so one past the end
|
|
84
112
|
// (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
|
-
|
|
113
|
+
// view from the layout, or, if every reference in a panel is bad, leave an
|
|
114
|
+
// empty panel with no clue why. An id names a view pinned with the spec's `id`
|
|
115
|
+
// key, so one nothing created is the same slip.
|
|
116
|
+
function unresolvedLayoutRefs(node, viewCount, knownIds) {
|
|
117
|
+
// Array-guarded rather than `?.`: a hand-written `views: 0` is non-nullish,
|
|
118
|
+
// so `?.` does not short-circuit and `(0).filter` threw a raw TypeError out
|
|
119
|
+
// of this validator — past `resolveLayoutSpec`, which has a real message for
|
|
120
|
+
// that exact slip and never got to say it.
|
|
88
121
|
return [
|
|
89
|
-
...(node.views
|
|
90
|
-
|
|
122
|
+
...(Array.isArray(node.views)
|
|
123
|
+
? node.views.filter(ref => typeof ref === 'number'
|
|
124
|
+
? ref < 0 || ref >= viewCount
|
|
125
|
+
: !knownIds.has(ref))
|
|
126
|
+
: []),
|
|
127
|
+
...(Array.isArray(node.children)
|
|
128
|
+
? node.children.flatMap(child => unresolvedLayoutRefs(child, viewCount, knownIds))
|
|
129
|
+
: []),
|
|
91
130
|
];
|
|
92
131
|
}
|
|
93
132
|
// A `tabs` node shares one cell between its children rather than dividing the
|
|
@@ -103,9 +142,9 @@ function outOfRangeLayoutIndices(node, viewCount) {
|
|
|
103
142
|
// nested or partial sizes had been ignored while the layout honoured them —
|
|
104
143
|
// which is also what website/docs/urlparams.md promises.
|
|
105
144
|
function unsizeableLayoutNodes(layout) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
return
|
|
145
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
146
|
+
const sizedTabsChild = layout.direction === 'tabs' && children.some(c => c.size !== undefined);
|
|
147
|
+
return sizedTabsChild || children.some(unsizeableLayoutNodes);
|
|
109
148
|
}
|
|
110
149
|
// The other statement a `tabs` node cannot take literally, and the one that
|
|
111
150
|
// used to fail silently rather than partially: a tab holds a FLAT stack of
|
|
@@ -115,9 +154,9 @@ function unsizeableLayoutNodes(layout) {
|
|
|
115
154
|
// to be dropped from the layout, after which homing swept them into whichever
|
|
116
155
|
// tab happened to be showing, so the arrangement was wrong with nothing said.
|
|
117
156
|
function flattenedTabsContainers(layout) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
return
|
|
157
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
158
|
+
const nestedTabsChild = layout.direction === 'tabs' && children.some(c => c.children !== undefined);
|
|
159
|
+
return nestedTabsChild || children.some(flattenedTabsContainers);
|
|
121
160
|
}
|
|
122
161
|
// use extension point named e.g. LaunchView-LinearGenomeView to initialize an
|
|
123
162
|
// LGV session
|
|
@@ -202,27 +241,47 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
202
241
|
if (noLauncher.length) {
|
|
203
242
|
session?.notifyError(`View type(s) ${noLauncher.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`);
|
|
204
243
|
}
|
|
205
|
-
//
|
|
206
|
-
//
|
|
244
|
+
// v4 nested a view's settings under `init`. A spec never becomes a
|
|
245
|
+
// snapshot, so `withLaunchInput`'s unwrap never runs on one and this
|
|
246
|
+
// surface does it itself, in the same order: the flat spelling wins.
|
|
247
|
+
const specViews = views.map(({ init, ...view }) => {
|
|
248
|
+
if (init) {
|
|
249
|
+
console.warn(legacyInitMessage(view.type));
|
|
250
|
+
}
|
|
251
|
+
return init ? { ...init, ...view } : view;
|
|
252
|
+
});
|
|
253
|
+
// The same classification a view snapshot gets, run here because a spec
|
|
254
|
+
// never becomes one: `LaunchView-<type>` takes these keys as arguments, so
|
|
255
|
+
// `withLaunchInput`'s partition never sees them and nothing names a typo.
|
|
256
|
+
// `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the
|
|
257
|
+
// launcher's downstream "No assembly provided", on the one surface written
|
|
258
|
+
// by hand with no compiler and no editor behind it.
|
|
259
|
+
//
|
|
260
|
+
// An ERROR rather than the snapshot path's warning: this surface's
|
|
261
|
+
// misplaced keys already report as errors, and the launcher's own failure
|
|
262
|
+
// lands as an error a line later — a warning under it reads as the lesser
|
|
263
|
+
// of the two, when it is the cause.
|
|
207
264
|
//
|
|
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
|
|
265
|
+
// A view type that registers no launch keys classifies nothing: its
|
|
266
|
+
// launcher's vocabulary is undeclared, so every argument would read as a
|
|
267
|
+
// typo.
|
|
213
268
|
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
269
|
+
// A lazily registered view type declares its properties on a state model
|
|
270
|
+
// that is not loaded yet, so it is loaded first — the launch below would
|
|
271
|
+
// have loaded it anyway.
|
|
272
|
+
await Promise.all(specViews
|
|
273
|
+
.filter(({ type }) => viewTypes.has(type))
|
|
274
|
+
.map(({ type }) => pluginManager.getViewType(type).loadStateModel()));
|
|
275
|
+
for (const { type, ...view } of specViews) {
|
|
276
|
+
const accepted = viewTypes.has(type)
|
|
277
|
+
? pluginManager.getViewType(type).acceptedKeys
|
|
278
|
+
: undefined;
|
|
279
|
+
const unknown = accepted
|
|
280
|
+
? Object.keys(view).filter(key => !accepted.includes(key))
|
|
281
|
+
: [];
|
|
282
|
+
if (unknown.length) {
|
|
283
|
+
session?.notifyError(unknownKeysMessage(type, unknown));
|
|
284
|
+
}
|
|
226
285
|
}
|
|
227
286
|
// Launch sequentially and record the id each spec view created, so the
|
|
228
287
|
// layout below can map its indices to real views. Reading session.views
|
|
@@ -230,13 +289,12 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
230
289
|
// synchronously and in order; capturing the delta per launch instead is
|
|
231
290
|
// correct even if a handler awaits or adds an auxiliary view, and lets a
|
|
232
291
|
// 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
|
-
//
|
|
292
|
+
// now already exists. `type` is the dispatch key, not a setting: forwarding
|
|
293
|
+
// it would land in the view's launch blob and trip the spurious
|
|
294
|
+
// "ignored unknown key(s): type" warning meant to catch typos.
|
|
236
295
|
// `displayName` is applied here rather than forwarded because it is a base
|
|
237
296
|
// 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.
|
|
297
|
+
// plugin-provided types whose launcher never heard of it).
|
|
240
298
|
// Let any connection this spec registered finish before the views that
|
|
241
299
|
// reference what it supplies are launched (see whenConnectionsSettle). Gated
|
|
242
300
|
// on there being views: with none, nothing is waiting on the connection, and
|
|
@@ -251,7 +309,7 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
251
309
|
return;
|
|
252
310
|
}
|
|
253
311
|
const createdViewIds = [];
|
|
254
|
-
for (const { type, displayName, ...view } of
|
|
312
|
+
for (const { type, displayName, ...view } of specViews) {
|
|
255
313
|
const before = new Set(session?.views.map(v => v.id));
|
|
256
314
|
// Strict so a launch handler that throws (missing/invalid assembly,
|
|
257
315
|
// unresolved track, ...) surfaces as a snackbar instead of being swallowed
|
|
@@ -268,17 +326,27 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
268
326
|
console.error(e);
|
|
269
327
|
session?.notifyError(`Failed to launch ${type} view: ${e}`, e);
|
|
270
328
|
}
|
|
271
|
-
const created = session?.views.
|
|
272
|
-
|
|
273
|
-
|
|
329
|
+
const created = session?.views.filter(v => !before.has(v.id)) ?? [];
|
|
330
|
+
// The entry's own view, when its launcher created others beside it: a
|
|
331
|
+
// connected ProteinView's name went to the genome view it opened first,
|
|
332
|
+
// and the structure came up "Untitled view".
|
|
333
|
+
const named = created.find(v => v.type === type) ?? created[0];
|
|
334
|
+
if (named && displayName) {
|
|
335
|
+
named.setDisplayName(displayName);
|
|
274
336
|
}
|
|
275
|
-
createdViewIds.push(created
|
|
337
|
+
createdViewIds.push(created.map(v => v.id));
|
|
276
338
|
}
|
|
277
339
|
if (layout && session) {
|
|
278
|
-
const
|
|
340
|
+
const knownIds = new Set(createdViewIds.flat());
|
|
341
|
+
const bad = unresolvedLayoutRefs(layout, views.length, knownIds);
|
|
342
|
+
const badIndices = bad.filter(ref => typeof ref === 'number');
|
|
343
|
+
const badIds = bad.filter(ref => typeof ref === 'string');
|
|
279
344
|
if (badIndices.length) {
|
|
280
345
|
session.notifyError(`Session spec layout references view index ${badIndices.join(', ')}, but the spec has ${views.length} view(s).`);
|
|
281
346
|
}
|
|
347
|
+
if (badIds.length) {
|
|
348
|
+
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).`);
|
|
349
|
+
}
|
|
282
350
|
if (unsizeableLayoutNodes(layout)) {
|
|
283
351
|
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
352
|
}
|
|
@@ -297,7 +365,20 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
297
365
|
// what `applyLayoutSpec`'s return value is for. Drop this call and the
|
|
298
366
|
// stated order is silently ignored: the tree holds it, nothing reads
|
|
299
367
|
// it, and the views come back in launch order with no diagnostic.
|
|
300
|
-
|
|
368
|
+
// Its own try/catch, the same reasoning as the per-view one above: the
|
|
369
|
+
// resolver throws for a layout it will not arrange, and this is the
|
|
370
|
+
// LAST thing the spec does — so an unarrangeable layout used to take
|
|
371
|
+
// the whole load's catch, after `setUseWorkspaces(true)` had already
|
|
372
|
+
// run, and report itself as the session's error with the views left
|
|
373
|
+
// unordered. The layout is one statement in the spec; losing it should
|
|
374
|
+
// cost the spec its layout and nothing else.
|
|
375
|
+
try {
|
|
376
|
+
session.orderViews(session.applyLayoutSpec(convertLayoutNode(layout, createdViewIds, knownIds, new Set(pinnedLayoutIds(layout, knownIds)))));
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
console.error(e);
|
|
380
|
+
session.notifyError(`Session spec layout: ${e}`, e);
|
|
381
|
+
}
|
|
301
382
|
}
|
|
302
383
|
else {
|
|
303
384
|
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
|
/**
|
|
@@ -1,7 +1,27 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* A caller-supplied view list narrowed to the views the session actually has.
|
|
10
|
+
*
|
|
11
|
+
* The list is the caller's snapshot, and `applyLayoutSpec` refuses an id no
|
|
12
|
+
* view has — so a view added after the list was built, or removed between
|
|
13
|
+
* building it and the call, would throw out of a sugar action no plugin wraps.
|
|
14
|
+
* The arrangement a stale name asks for is still the one it means for every
|
|
15
|
+
* view that IS there, so the name is dropped rather than the action. A repeat
|
|
16
|
+
* is dropped for the same reason: it would seat one view in two cells, which
|
|
17
|
+
* the resolver also refuses.
|
|
18
|
+
*/
|
|
19
|
+
function liveViewIds(self, allViewIds) {
|
|
20
|
+
const live = sessionViewIds(self);
|
|
21
|
+
return [
|
|
22
|
+
...new Set(live ? allViewIds.filter(id => live.includes(id)) : allViewIds),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* #stateModel WorkspaceLayoutMixin
|
|
7
27
|
*
|
|
@@ -210,6 +230,13 @@ export function WorkspaceLayoutMixin() {
|
|
|
210
230
|
* Returns where the view came from, or `undefined` if it is not in the
|
|
211
231
|
* session, in which case nothing is applied.
|
|
212
232
|
*/
|
|
233
|
+
function everyViewId(action, allViewIds) {
|
|
234
|
+
const ids = allViewIds ?? sessionViewIds(self);
|
|
235
|
+
if (!ids) {
|
|
236
|
+
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`);
|
|
237
|
+
}
|
|
238
|
+
return ids;
|
|
239
|
+
}
|
|
213
240
|
function rehomeView(viewId, allViewIds, place) {
|
|
214
241
|
const tree = home(self.tree, allViewIds);
|
|
215
242
|
const from = tabContainingView(tree, viewId);
|
|
@@ -400,6 +427,14 @@ export function WorkspaceLayoutMixin() {
|
|
|
400
427
|
/**
|
|
401
428
|
* Arrange the workspace as a spec states.
|
|
402
429
|
*
|
|
430
|
+
* The spec is the same shape a session spec's `layout` takes: a leaf's
|
|
431
|
+
* `views` names views by index into `session.views` or by id, so
|
|
432
|
+
* `{ direction: 'horizontal', children: [{ views: [0] }, { views: [1] }] }`
|
|
433
|
+
* means here what it means in a link. A leaf spelled any other way, an
|
|
434
|
+
* index past the end or an id no view has throws rather than arranging
|
|
435
|
+
* nothing — an untyped caller used to collapse the workspace into one
|
|
436
|
+
* blank tab that way.
|
|
437
|
+
*
|
|
403
438
|
* There is no `init` property and no standing request: the spec is
|
|
404
439
|
* converted and *becomes* the layout, here and now. `init` existed only
|
|
405
440
|
* because dockview had to be told, could not be told before it mounted,
|
|
@@ -407,20 +442,21 @@ export function WorkspaceLayoutMixin() {
|
|
|
407
442
|
* from the layout living somewhere this action could not reach.
|
|
408
443
|
*/
|
|
409
444
|
applyLayoutSpec(spec) {
|
|
410
|
-
|
|
445
|
+
const resolved = resolveLayoutSpec(spec, sessionViewIds(self));
|
|
446
|
+
apply(treeFromSpec(resolved, nextId));
|
|
411
447
|
self.activePanelId = panels(self.tree)[0]?.id;
|
|
412
|
-
return viewIdsInSpec(
|
|
448
|
+
return viewIdsInSpec(resolved);
|
|
413
449
|
},
|
|
414
450
|
/**
|
|
415
451
|
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
416
452
|
*
|
|
417
|
-
* `allViewIds` is EVERY view in the session,
|
|
418
|
-
*
|
|
453
|
+
* `allViewIds` is EVERY view in the session, defaulting to the host's
|
|
454
|
+
* own list — homing drops any view the list does not name, so the
|
|
419
455
|
* `[viewId]` default this used to carry unhomed all the others.
|
|
420
456
|
*/
|
|
421
457
|
moveViewToNewTab(viewId, allViewIds) {
|
|
422
458
|
const tab = { id: nextId('tab'), viewIds: [viewId] };
|
|
423
|
-
const from = rehomeView(viewId, allViewIds, (tree, at) => addTab(tree, at.panel.id, tab));
|
|
459
|
+
const from = rehomeView(viewId, everyViewId('moveViewToNewTab', allViewIds), (tree, at) => addTab(tree, at.panel.id, tab));
|
|
424
460
|
if (!from) {
|
|
425
461
|
return undefined;
|
|
426
462
|
}
|
|
@@ -439,7 +475,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
439
475
|
tabs: [{ id: tabId, viewIds: [viewId] }],
|
|
440
476
|
activeTabId: tabId,
|
|
441
477
|
};
|
|
442
|
-
const from = rehomeView(viewId, allViewIds, (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
|
|
478
|
+
const from = rehomeView(viewId, everyViewId('moveViewToSplitRight', allViewIds), (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
|
|
443
479
|
if (!from) {
|
|
444
480
|
return undefined;
|
|
445
481
|
}
|
|
@@ -480,21 +516,20 @@ export function WorkspaceLayoutMixin() {
|
|
|
480
516
|
* is spelled.
|
|
481
517
|
*/
|
|
482
518
|
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
519
|
// Not `?? []`: homing drops every view the list does not name, so an
|
|
491
520
|
// empty one would answer "put this view beside nothing" and unhome the
|
|
492
521
|
// rest. Nothing to say is nothing to do.
|
|
493
|
-
const ids = allViewIds ??
|
|
522
|
+
const ids = allViewIds ?? sessionViewIds(self);
|
|
494
523
|
if (!move || !ids) {
|
|
495
524
|
return;
|
|
496
525
|
}
|
|
497
|
-
self
|
|
526
|
+
const known = liveViewIds(self, ids);
|
|
527
|
+
// Nothing to move: the view named is not in the session, so there is
|
|
528
|
+
// no arrangement to state about it.
|
|
529
|
+
if (sessionViewIds(self) && !known.includes(move.viewId)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
self.applyLayoutSpec(specForPendingMove(move, known));
|
|
498
533
|
// Show where the view went. A spec states an arrangement and not a
|
|
499
534
|
// selection, so `treeFromSpec` shows each cell's FIRST tab — and
|
|
500
535
|
// `newTab` puts the moved view in a tab beside the others, which makes
|
|
@@ -518,7 +553,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
518
553
|
* session spec's layout there is nothing for `orderViews` to apply.
|
|
519
554
|
*/
|
|
520
555
|
tileViews(mode, allViewIds) {
|
|
521
|
-
self.applyLayoutSpec(tileLayoutSpec(allViewIds, mode));
|
|
556
|
+
self.applyLayoutSpec(tileLayoutSpec(liveViewIds(self, allViewIds), mode));
|
|
522
557
|
},
|
|
523
558
|
})));
|
|
524
559
|
}
|
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
import type { LayoutTree, NodeKind } from './tree.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A view named by a layout leaf: an index into the view list the layout is
|
|
4
|
+
* applied against (a session spec's own `views`, or `session.views` for the
|
|
5
|
+
* live `applyLayoutSpec`), or a view id.
|
|
6
|
+
*/
|
|
7
|
+
export type LayoutViewRef = number | string;
|
|
2
8
|
/**
|
|
3
9
|
* The `layout` a session spec / URL param states, in the vocabulary users
|
|
4
|
-
* write it in
|
|
5
|
-
* so
|
|
6
|
-
*
|
|
10
|
+
* write it in, and the same shape the live `applyLayoutSpec` action takes — one
|
|
11
|
+
* shape, so an agent that has written one has written the other. **This shape
|
|
12
|
+
* is public** — it is documented as a URL parameter — so it keeps
|
|
13
|
+
* `horizontal`/`vertical`/`tabs` and percentage `size` rather than being
|
|
14
|
+
* renamed to match the internal tree.
|
|
15
|
+
*
|
|
16
|
+
* `resolveLayoutSpec` turns the indexes into ids; the tree builder below takes
|
|
17
|
+
* the resolved form only.
|
|
7
18
|
*/
|
|
8
|
-
export interface LayoutSpecNode {
|
|
19
|
+
export interface LayoutSpecNode<View = LayoutViewRef> {
|
|
9
20
|
/** a leaf: the views to stack vertically in one tab */
|
|
10
|
-
|
|
21
|
+
views?: View[];
|
|
11
22
|
/** how children divide the space; `tabs` puts them in one cell as tabs */
|
|
12
23
|
direction?: 'horizontal' | 'vertical' | 'tabs';
|
|
13
|
-
children?: LayoutSpecNode[];
|
|
24
|
+
children?: LayoutSpecNode<View>[];
|
|
14
25
|
/** share of the parent, as a percentage */
|
|
15
26
|
size?: number;
|
|
16
27
|
}
|
|
28
|
+
export type ResolvedLayoutSpecNode = LayoutSpecNode<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve every view a spec names to an id against `allViewIds`, the list its
|
|
31
|
+
* indexes count into. Anything that resolves to nothing throws, naming what was
|
|
32
|
+
* received: an untyped caller (the MCP `run_javascript` tool) that writes
|
|
33
|
+
* `viewIds` where `views` goes, or an index past the end, used to be accepted
|
|
34
|
+
* as an empty leaf and collapse the workspace into one blank tab with nothing
|
|
35
|
+
* said.
|
|
36
|
+
*
|
|
37
|
+
* A node stating no `views` and no `children` is NOT that slip — it is the
|
|
38
|
+
* empty panel `treeFromSpec` has always built, and refusing it here made the
|
|
39
|
+
* two surfaces disagree about the one shape they share. The slip is an
|
|
40
|
+
* unrecognized KEY, which is what `viewIds` actually is, and naming it beats
|
|
41
|
+
* the old message that could only say the node needed something else.
|
|
42
|
+
*
|
|
43
|
+
* `allViewIds` is undefined only on a model that composes no view list, where
|
|
44
|
+
* an id cannot be checked and an index cannot mean anything.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveLayoutSpec(spec: LayoutSpecNode, allViewIds: string[] | undefined): ResolvedLayoutSpecNode;
|
|
17
47
|
/** A request to move one view relative to the others. Public plugin API. */
|
|
18
48
|
export interface PendingMove {
|
|
19
49
|
type: 'newTab' | 'splitRight';
|
|
@@ -29,9 +59,9 @@ export interface PendingMove {
|
|
|
29
59
|
* nesting, so `size` applies wherever it is written and there is no all-or-
|
|
30
60
|
* nothing pass.
|
|
31
61
|
*/
|
|
32
|
-
export declare function treeFromSpec(spec:
|
|
62
|
+
export declare function treeFromSpec(spec: ResolvedLayoutSpecNode, nextId: (kind: NodeKind) => string): LayoutTree;
|
|
33
63
|
/** Every viewId a spec names, depth-first — the order it states. */
|
|
34
|
-
export declare function viewIdsInSpec(spec:
|
|
64
|
+
export declare function viewIdsInSpec(spec: ResolvedLayoutSpecNode): string[];
|
|
35
65
|
/** How "arrange everything" lays the whole session out. */
|
|
36
66
|
export type TileMode = 'tabs' | 'horizontal' | 'vertical' | 'grid';
|
|
37
67
|
/**
|
|
@@ -49,10 +79,10 @@ export type TileMode = 'tabs' | 'horizontal' | 'vertical' | 'grid';
|
|
|
49
79
|
* width, since `resolveSizes` reads bare siblings as an even share of their own
|
|
50
80
|
* branch rather than of the grid.
|
|
51
81
|
*/
|
|
52
|
-
export declare function tileLayoutSpec(viewIds: string[], mode: TileMode):
|
|
82
|
+
export declare function tileLayoutSpec(viewIds: string[], mode: TileMode): ResolvedLayoutSpecNode;
|
|
53
83
|
/**
|
|
54
84
|
* `setPendingMove` as a spec: everything else keeps its side, the named view
|
|
55
85
|
* takes the other. With nothing else on screen there is nothing to split from,
|
|
56
86
|
* so the view just takes the space.
|
|
57
87
|
*/
|
|
58
|
-
export declare function specForPendingMove(move: PendingMove, allViewIds: string[]):
|
|
88
|
+
export declare function specForPendingMove(move: PendingMove, allViewIds: string[]): ResolvedLayoutSpecNode;
|