@jbrowse/app-core 5.0.0-beta.1 → 5.0.0-beta.11
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/AppSession/AppSessionMixin.d.ts +42 -316
- package/esm/Assemblies/AssembliesMixin.d.ts +172 -103
- package/esm/Assemblies/AssembliesMixin.js +13 -8
- package/esm/HistoryManagement/index.d.ts +6 -11
- package/esm/HistoryManagement/index.js +24 -28
- package/esm/JBrowseConfig/RootConfiguration.d.ts +80 -124
- package/esm/JBrowseConfig/RootConfiguration.js +5 -11
- package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
- package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
- package/esm/JBrowseConfig/index.d.ts +89 -83
- package/esm/JBrowseConfig/index.js +6 -4
- package/esm/JBrowseModel/index.d.ts +93 -96
- package/esm/JBrowseModel/index.js +43 -43
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +2767 -0
- package/esm/JbApi/jbApi.js +1383 -0
- package/esm/RootMenu/index.d.ts +2 -11
- package/esm/RootMenu/index.js +0 -11
- package/esm/SessionSpec/index.d.ts +1 -1
- package/esm/SessionSpec/index.js +1 -1
- package/esm/SessionSpec/lgvUrlInit.js +5 -2
- package/esm/SessionSpec/loadSessionSpec.d.ts +56 -2
- package/esm/SessionSpec/loadSessionSpec.js +285 -123
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +2 -8
- package/esm/SessionSpec/parseSessionSpecUrl.js +2 -17
- package/esm/SessionSpec/types.d.ts +16 -12
- package/esm/WorkspaceLayout/WorkspaceContainer.js +4 -2
- package/esm/WorkspaceLayout/WorkspaceTab.js +2 -6
- package/esm/WorkspaceLayout/model.d.ts +25 -5
- package/esm/WorkspaceLayout/model.js +77 -18
- package/esm/WorkspaceLayout/spec.d.ts +40 -10
- package/esm/WorkspaceLayout/spec.js +81 -8
- package/esm/index.d.ts +4 -2
- package/esm/index.js +4 -1
- package/esm/menus.d.ts +15 -29
- package/esm/menus.js +76 -92
- package/esm/queryParams.d.ts +1 -1
- package/esm/queryParams.js +10 -9
- package/esm/ui/App/App.js +18 -11
- package/esm/ui/App/AppToolbar.js +23 -8
- package/esm/ui/App/ClassicViewsContainer.d.ts +2 -2
- package/esm/ui/App/DialogQueue.d.ts +2 -2
- package/esm/ui/App/DialogQueue.js +3 -3
- package/esm/ui/App/DrawerHeader.js +2 -3
- package/esm/ui/App/ThemeModeButton.d.ts +12 -0
- package/esm/ui/App/ThemeModeButton.js +34 -0
- package/esm/ui/App/ViewButtons.js +2 -2
- package/esm/ui/App/ViewContainer.js +1 -17
- package/esm/ui/App/ViewContainerTitle.js +15 -12
- package/esm/ui/App/ViewLauncher.js +1 -1
- package/esm/ui/App/ViewMenu.js +2 -2
- package/esm/ui/App/ViewStack.js +7 -1
- package/esm/ui/App/ViewsContainer.d.ts +2 -4
- package/esm/ui/App/ViewsContainer.js +3 -9
- package/esm/ui/App/index.d.ts +1 -1
- package/esm/ui/App/index.js +1 -1
- package/esm/ui/App/lazyParts.d.ts +32 -0
- package/esm/ui/App/lazyParts.js +8 -0
- package/esm/ui/App/preloadAppShell.d.ts +14 -0
- package/esm/ui/App/preloadAppShell.js +41 -0
- package/esm/ui/App/types.d.ts +12 -7
- package/esm/ui/App/useViewVisibility.d.ts +10 -9
- package/esm/ui/App/useViewVisibility.js +12 -11
- package/esm/ui/App/viewTitle.d.ts +10 -3
- package/esm/ui/App/viewTitle.js +13 -5
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +7 -7
- package/esm/ui/App/AppReadyMarker.d.ts +0 -34
- package/esm/ui/App/AppReadyMarker.js +0 -56
- package/esm/ui/App/DrawerControls.d.ts +0 -1
- package/esm/ui/App/DrawerControls.js +0 -1
- package/esm/ui/App/copyView.d.ts +0 -1
- package/esm/ui/App/copyView.js +0 -29
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,5 +1,5 @@
|
|
|
1
1
|
export { buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './lgvUrlInit.ts';
|
|
2
|
-
export { addSessionTracks, loadSessionSpec } from './loadSessionSpec.ts';
|
|
2
|
+
export { addSessionTracks, launchSpecView, launchableSpecView, loadSessionSpec, viewTypeProblem, } from './loadSessionSpec.ts';
|
|
3
3
|
export { parseSessionSpecUrl } from './parseSessionSpecUrl.ts';
|
|
4
4
|
export type { LgvUrlInit } from './lgvUrlInit.ts';
|
|
5
5
|
export type { ParsedSessionSpec } from './parseSessionSpecUrl.ts';
|
package/esm/SessionSpec/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './lgvUrlInit.js';
|
|
2
|
-
export { addSessionTracks, loadSessionSpec } from './loadSessionSpec.js';
|
|
2
|
+
export { addSessionTracks, launchSpecView, launchableSpecView, loadSessionSpec, viewTypeProblem, } from './loadSessionSpec.js';
|
|
3
3
|
export { parseSessionSpecUrl } from './parseSessionSpecUrl.js';
|
|
@@ -66,8 +66,11 @@ export function buildLgvInit(args) {
|
|
|
66
66
|
if (assembly !== undefined) {
|
|
67
67
|
init.assembly = assembly;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// empty entries dropped, like `regions`: `&tracks=a,` asked for no track
|
|
70
|
+
// named ''
|
|
71
|
+
const trackIds = tracks?.split(',').filter(Boolean);
|
|
72
|
+
if (trackIds?.length) {
|
|
73
|
+
init.tracks = trackIds;
|
|
71
74
|
}
|
|
72
75
|
if (tracklist !== undefined) {
|
|
73
76
|
init.tracklist = tracklist;
|
|
@@ -11,11 +11,65 @@ import type { AbstractSessionModel } from '@jbrowse/core/util';
|
|
|
11
11
|
* layer down.
|
|
12
12
|
*/
|
|
13
13
|
export declare function addSessionTracks(session: AbstractSessionModel | undefined, tracks: Record<string, unknown>[]): void;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Why one view type cannot launch from a spec here, or nothing. Two causes,
|
|
16
|
+
* two messages: the type is unknown at all (a typo, or a plugin that was not
|
|
17
|
+
* loaded), or it exists but nothing taught it how to launch from a spec, which
|
|
18
|
+
* only the view's plugin can fix.
|
|
19
|
+
*/
|
|
20
|
+
export declare function viewTypeProblem(pluginManager: PluginManager, type: string): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A spec view entry as its launcher takes it, and what its type does not take.
|
|
23
|
+
*
|
|
24
|
+
* The same classification a view snapshot gets, run here because a spec never
|
|
25
|
+
* becomes one: `LaunchView-<type>` takes these keys as arguments, so
|
|
26
|
+
* `withLaunchInput`'s partition never sees them and nothing names a typo.
|
|
27
|
+
* `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the launcher's
|
|
28
|
+
* downstream "No assembly provided", on the one surface written by hand with no
|
|
29
|
+
* compiler and no editor behind it.
|
|
30
|
+
*
|
|
31
|
+
* A view type that registers no launch keys classifies nothing: its launcher's
|
|
32
|
+
* vocabulary is undeclared, so every argument would read as a typo. A lazily
|
|
33
|
+
* registered type declares its properties on a state model that is not loaded
|
|
34
|
+
* yet, so it is loaded first — the launch would have loaded it anyway.
|
|
35
|
+
*/
|
|
36
|
+
export declare function launchableSpecView(pluginManager: PluginManager, spec: ViewSpec): Promise<{
|
|
37
|
+
view: ViewSpec;
|
|
38
|
+
problem?: string;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Launch one spec view into the session through its `LaunchView-<type>`
|
|
42
|
+
* extension point, the same door `loadSessionSpec` uses for each of its views
|
|
43
|
+
* — so `jb.addView` and a spec agree on what a view entry means, a
|
|
44
|
+
* ProteinView's `connectedView` included.
|
|
45
|
+
*
|
|
46
|
+
* Strict so a launch handler that throws (missing/invalid assembly, unresolved
|
|
47
|
+
* track, ...) is returned as a failure instead of being swallowed by the plain
|
|
48
|
+
* extension-point runner, which would leave a silent empty session. A handler
|
|
49
|
+
* that legitimately no-ops returns normally and is unaffected.
|
|
50
|
+
*
|
|
51
|
+
* `created` is EVERY view the launch added, in creation order: a connected
|
|
52
|
+
* ProteinView creates its genome view and then itself. `view` is the entry's
|
|
53
|
+
* own, the one of the spec's type, which is where `displayName` goes — it used
|
|
54
|
+
* to land on the genome view a ProteinView opened first, and the structure came
|
|
55
|
+
* up "Untitled view". `type` is the dispatch key, not a setting: forwarding it
|
|
56
|
+
* would land in the view's launch blob and trip the "ignored unknown key(s):
|
|
57
|
+
* type" warning meant to catch typos.
|
|
58
|
+
*/
|
|
59
|
+
export declare function launchSpecView(session: AbstractSessionModel | undefined, pluginManager: PluginManager, spec: ViewSpec): Promise<{
|
|
60
|
+
created: import("@jbrowse/core/util").AbstractViewModel[];
|
|
61
|
+
view: import("@jbrowse/core/util").AbstractViewModel | undefined;
|
|
62
|
+
failure?: {
|
|
63
|
+
message: string;
|
|
64
|
+
cause: unknown;
|
|
65
|
+
} | undefined;
|
|
66
|
+
}>;
|
|
67
|
+
export interface SessionSpec {
|
|
15
68
|
views: ViewSpec[];
|
|
16
69
|
sessionAssemblies?: Record<string, unknown>[];
|
|
17
70
|
sessionConnections?: Record<string, unknown>[];
|
|
18
71
|
sessionTracks?: Record<string, unknown>[];
|
|
19
72
|
layout?: LayoutNode;
|
|
20
73
|
sessionName?: string;
|
|
21
|
-
}
|
|
74
|
+
}
|
|
75
|
+
export declare function loadSessionSpec(spec: SessionSpec, pluginManager: PluginManager): Promise<void>;
|
|
@@ -1,10 +1,14 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// A spec `layout` needs `layoutViews`, which a session composing
|
|
6
|
+
// WorkspaceLayoutMixin and MultipleViewsSessionMixin has and an embedded
|
|
7
|
+
// product's does not. It is looked up behind an `in` guard, so renaming it
|
|
8
|
+
// without renaming it here makes every spec layout silently decline rather than
|
|
9
|
+
// fail to compile; see app-core/src/WorkspaceLayout/CLAUDE.md.
|
|
10
|
+
function hasLayoutViews(session) {
|
|
11
|
+
return 'layoutViews' in session;
|
|
8
12
|
}
|
|
9
13
|
function isSessionWithSpecConnections(session) {
|
|
10
14
|
return ('addConnectionConf' in session &&
|
|
@@ -57,37 +61,75 @@ async function whenConnectionsSettle(session) {
|
|
|
57
61
|
await when(() => gone() || session.connectionInstances.every(conn => !conn.loading));
|
|
58
62
|
return !gone();
|
|
59
63
|
}
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
// Resolve a spec layout's view indexes to ids before `applyLayoutSpec` counts
|
|
65
|
+
// them into `session.views`, which is a different list: a spec index names a
|
|
66
|
+
// spec entry, and `createdViewIds[i]` is EVERY view the i-th entry created, in
|
|
67
|
+
// creation order. A connected ProteinView creates its genome view and then
|
|
68
|
+
// itself, and the index names both — stacked in one cell — where recording one
|
|
69
|
+
// id per entry left the structure unaddressable, in no cell, and the panel
|
|
70
|
+
// naming it empty. A string is a view id and passes through; a reference that
|
|
71
|
+
// resolves to nothing is dropped here after `unresolvedLayoutRefs` reported it.
|
|
72
|
+
function convertLayoutNode(node, createdViewIds, knownIds, pinnedIds) {
|
|
73
|
+
const { views, children, ...rest } = node;
|
|
74
|
+
return {
|
|
75
|
+
...rest,
|
|
76
|
+
...(views === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: {
|
|
79
|
+
views: Array.isArray(views)
|
|
80
|
+
? views.flatMap(ref => typeof ref === 'number'
|
|
81
|
+
? (createdViewIds[ref] ?? []).filter(id => !pinnedIds.has(id))
|
|
82
|
+
: knownIds.has(ref)
|
|
83
|
+
? [ref]
|
|
84
|
+
: [])
|
|
85
|
+
: views,
|
|
86
|
+
}),
|
|
87
|
+
...(children === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: {
|
|
90
|
+
children: Array.isArray(children)
|
|
91
|
+
? children.map(child => convertLayoutNode(child, createdViewIds, knownIds, pinnedIds))
|
|
92
|
+
: children,
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// An index names every view its entry created, so the entry that opens a genome
|
|
97
|
+
// view beside its own puts BOTH in the cell naming it — which is the point, and
|
|
98
|
+
// is wrong the moment another cell names one of them by id. `{views:[0]}` next
|
|
99
|
+
// to `{views:['structure']}` is the obvious spelling of "genome left, structure
|
|
100
|
+
// right", and expanding the index blind seats the structure in both cells: two
|
|
101
|
+
// React trees and two GPU contexts for one model, which `resolveLayoutSpec` now
|
|
102
|
+
// refuses outright. An id stated by hand is the more specific statement, so it
|
|
103
|
+
// wins its view and the index gives it up.
|
|
104
|
+
function pinnedLayoutIds(node, knownIds) {
|
|
105
|
+
return [
|
|
106
|
+
...(Array.isArray(node.views)
|
|
107
|
+
? node.views.filter((ref) => typeof ref === 'string' && knownIds.has(ref))
|
|
108
|
+
: []),
|
|
109
|
+
...(Array.isArray(node.children)
|
|
110
|
+
? node.children.flatMap(child => pinnedLayoutIds(child, knownIds))
|
|
111
|
+
: []),
|
|
112
|
+
];
|
|
82
113
|
}
|
|
83
114
|
// A layout index is a position in the spec's `views` array, so one past the end
|
|
84
115
|
// (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
|
-
|
|
116
|
+
// view from the layout, or, if every reference in a panel is bad, leave an
|
|
117
|
+
// empty panel with no clue why. An id names a view pinned with the spec's `id`
|
|
118
|
+
// key, so one nothing created is the same slip.
|
|
119
|
+
function unresolvedLayoutRefs(node, viewCount, knownIds) {
|
|
120
|
+
// Array-guarded rather than `?.`: a hand-written `views: 0` is non-nullish,
|
|
121
|
+
// so `?.` does not short-circuit and `(0).filter` threw a raw TypeError out
|
|
122
|
+
// of this validator — past `resolveLayoutSpec`, which has a real message for
|
|
123
|
+
// that exact slip and never got to say it.
|
|
88
124
|
return [
|
|
89
|
-
...(node.views
|
|
90
|
-
|
|
125
|
+
...(Array.isArray(node.views)
|
|
126
|
+
? node.views.filter(ref => typeof ref === 'number'
|
|
127
|
+
? ref < 0 || ref >= viewCount
|
|
128
|
+
: !knownIds.has(ref))
|
|
129
|
+
: []),
|
|
130
|
+
...(Array.isArray(node.children)
|
|
131
|
+
? node.children.flatMap(child => unresolvedLayoutRefs(child, viewCount, knownIds))
|
|
132
|
+
: []),
|
|
91
133
|
];
|
|
92
134
|
}
|
|
93
135
|
// A `tabs` node shares one cell between its children rather than dividing the
|
|
@@ -103,9 +145,9 @@ function outOfRangeLayoutIndices(node, viewCount) {
|
|
|
103
145
|
// nested or partial sizes had been ignored while the layout honoured them —
|
|
104
146
|
// which is also what website/docs/urlparams.md promises.
|
|
105
147
|
function unsizeableLayoutNodes(layout) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
return
|
|
148
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
149
|
+
const sizedTabsChild = layout.direction === 'tabs' && children.some(c => c.size !== undefined);
|
|
150
|
+
return sizedTabsChild || children.some(unsizeableLayoutNodes);
|
|
109
151
|
}
|
|
110
152
|
// The other statement a `tabs` node cannot take literally, and the one that
|
|
111
153
|
// used to fail silently rather than partially: a tab holds a FLAT stack of
|
|
@@ -115,25 +157,179 @@ function unsizeableLayoutNodes(layout) {
|
|
|
115
157
|
// to be dropped from the layout, after which homing swept them into whichever
|
|
116
158
|
// tab happened to be showing, so the arrangement was wrong with nothing said.
|
|
117
159
|
function flattenedTabsContainers(layout) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
return
|
|
160
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
161
|
+
const nestedTabsChild = layout.direction === 'tabs' && children.some(c => c.children !== undefined);
|
|
162
|
+
return nestedTabsChild || children.some(flattenedTabsContainers);
|
|
163
|
+
}
|
|
164
|
+
function unknownViewTypesMessage(types) {
|
|
165
|
+
return `Unknown view type(s) in session spec: ${types.join(', ')}. The plugin providing the view may be missing, or the type may be misspelled.`;
|
|
166
|
+
}
|
|
167
|
+
function noLauncherMessage(types) {
|
|
168
|
+
return `View type(s) ${types.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Why one view type cannot launch from a spec here, or nothing. Two causes,
|
|
172
|
+
* two messages: the type is unknown at all (a typo, or a plugin that was not
|
|
173
|
+
* loaded), or it exists but nothing taught it how to launch from a spec, which
|
|
174
|
+
* only the view's plugin can fix.
|
|
175
|
+
*/
|
|
176
|
+
export function viewTypeProblem(pluginManager, type) {
|
|
177
|
+
if (pluginManager.extensionPoints.has(`LaunchView-${type}`)) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
// the record's `has`, not getElementType, which throws on an unregistered
|
|
181
|
+
// name rather than returning undefined
|
|
182
|
+
return pluginManager.getElementTypeRecord('view').has(type)
|
|
183
|
+
? noLauncherMessage([type])
|
|
184
|
+
: unknownViewTypesMessage([type]);
|
|
185
|
+
}
|
|
186
|
+
// v4 nested a view's settings under `init`. A spec never becomes a snapshot, so
|
|
187
|
+
// `withLaunchInput`'s unwrap never runs on one and this surface does it itself,
|
|
188
|
+
// in the same order: the flat spelling wins.
|
|
189
|
+
function flattenSpecView(spec) {
|
|
190
|
+
const { init, ...view } = spec;
|
|
191
|
+
if (init) {
|
|
192
|
+
console.warn(legacyInitMessage(view.type));
|
|
193
|
+
}
|
|
194
|
+
return init ? { ...init, ...view } : view;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A spec view entry as its launcher takes it, and what its type does not take.
|
|
198
|
+
*
|
|
199
|
+
* The same classification a view snapshot gets, run here because a spec never
|
|
200
|
+
* becomes one: `LaunchView-<type>` takes these keys as arguments, so
|
|
201
|
+
* `withLaunchInput`'s partition never sees them and nothing names a typo.
|
|
202
|
+
* `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the launcher's
|
|
203
|
+
* downstream "No assembly provided", on the one surface written by hand with no
|
|
204
|
+
* compiler and no editor behind it.
|
|
205
|
+
*
|
|
206
|
+
* A view type that registers no launch keys classifies nothing: its launcher's
|
|
207
|
+
* vocabulary is undeclared, so every argument would read as a typo. A lazily
|
|
208
|
+
* registered type declares its properties on a state model that is not loaded
|
|
209
|
+
* yet, so it is loaded first — the launch would have loaded it anyway.
|
|
210
|
+
*/
|
|
211
|
+
export async function launchableSpecView(pluginManager, spec) {
|
|
212
|
+
const flat = flattenSpecView(spec);
|
|
213
|
+
if (!pluginManager.getElementTypeRecord('view').has(flat.type)) {
|
|
214
|
+
return { view: flat };
|
|
215
|
+
}
|
|
216
|
+
const viewType = pluginManager.getViewType(flat.type);
|
|
217
|
+
await viewType.loadStateModel();
|
|
218
|
+
const accepted = viewType.acceptedKeys;
|
|
219
|
+
if (!accepted) {
|
|
220
|
+
return { view: flat };
|
|
221
|
+
}
|
|
222
|
+
const { view, problem } = readAssemblyNames(flat, accepted);
|
|
223
|
+
const { type, ...keyed } = view;
|
|
224
|
+
const unknown = Object.keys(keyed).filter(key => !accepted.includes(key));
|
|
225
|
+
const problems = [
|
|
226
|
+
...(unknown.length ? [unknownKeysMessage(type, unknown)] : []),
|
|
227
|
+
...(problem ? [problem] : []),
|
|
228
|
+
];
|
|
229
|
+
return { view, problem: problems.join('; ') || undefined };
|
|
230
|
+
}
|
|
231
|
+
// An agent reads `assemblyNames` off the live view and writes it back into a
|
|
232
|
+
// spec, where the key the entry takes is `assembly`. One name is that assembly;
|
|
233
|
+
// several are a question one view cannot answer.
|
|
234
|
+
function readAssemblyNames(view, accepted) {
|
|
235
|
+
const { assemblyNames, ...rest } = view;
|
|
236
|
+
// Array.isArray, not a length check: a hand-written `assemblyNames: 'volvox'`
|
|
237
|
+
// has a length too, and reached .join below — a throw, which this one outer
|
|
238
|
+
// catch turns into every remaining view and the layout being lost
|
|
239
|
+
if (!Array.isArray(assemblyNames) ||
|
|
240
|
+
!assemblyNames.length ||
|
|
241
|
+
!accepted.includes('assembly') ||
|
|
242
|
+
accepted.includes('assemblyNames')) {
|
|
243
|
+
return { view };
|
|
244
|
+
}
|
|
245
|
+
return assemblyNames.length === 1
|
|
246
|
+
? { view: { ...rest, assembly: rest.assembly ?? assemblyNames[0] } }
|
|
247
|
+
: {
|
|
248
|
+
view: rest,
|
|
249
|
+
problem: `${view.type} takes one "assembly", and assemblyNames names ${assemblyNames.length} (${assemblyNames.join(', ')}) — pass assembly, one view per assembly`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Launch one spec view into the session through its `LaunchView-<type>`
|
|
254
|
+
* extension point, the same door `loadSessionSpec` uses for each of its views
|
|
255
|
+
* — so `jb.addView` and a spec agree on what a view entry means, a
|
|
256
|
+
* ProteinView's `connectedView` included.
|
|
257
|
+
*
|
|
258
|
+
* Strict so a launch handler that throws (missing/invalid assembly, unresolved
|
|
259
|
+
* track, ...) is returned as a failure instead of being swallowed by the plain
|
|
260
|
+
* extension-point runner, which would leave a silent empty session. A handler
|
|
261
|
+
* that legitimately no-ops returns normally and is unaffected.
|
|
262
|
+
*
|
|
263
|
+
* `created` is EVERY view the launch added, in creation order: a connected
|
|
264
|
+
* ProteinView creates its genome view and then itself. `view` is the entry's
|
|
265
|
+
* own, the one of the spec's type, which is where `displayName` goes — it used
|
|
266
|
+
* to land on the genome view a ProteinView opened first, and the structure came
|
|
267
|
+
* up "Untitled view". `type` is the dispatch key, not a setting: forwarding it
|
|
268
|
+
* would land in the view's launch blob and trip the "ignored unknown key(s):
|
|
269
|
+
* type" warning meant to catch typos.
|
|
270
|
+
*/
|
|
271
|
+
export async function launchSpecView(session, pluginManager, spec) {
|
|
272
|
+
const { type, displayName, ...view } = spec;
|
|
273
|
+
const before = new Set(session?.views.map(v => v.id));
|
|
274
|
+
let failure;
|
|
275
|
+
try {
|
|
276
|
+
await pluginManager.evaluateAsyncExtensionPointStrict(`LaunchView-${type}`, {
|
|
277
|
+
...view,
|
|
278
|
+
session,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
console.error(e);
|
|
283
|
+
failure = { message: `Failed to launch ${type} view: ${e}`, cause: e };
|
|
284
|
+
}
|
|
285
|
+
const created = session?.views.filter(v => !before.has(v.id)) ?? [];
|
|
286
|
+
const named = created.find(v => v.type === type) ?? created[0];
|
|
287
|
+
if (named && displayName) {
|
|
288
|
+
named.setDisplayName(displayName);
|
|
289
|
+
}
|
|
290
|
+
return { created, view: named, ...(failure ? { failure } : {}) };
|
|
121
291
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
292
|
+
const SPEC_KEYS = [
|
|
293
|
+
'views',
|
|
294
|
+
'sessionAssemblies',
|
|
295
|
+
'sessionConnections',
|
|
296
|
+
'sessionTracks',
|
|
297
|
+
'layout',
|
|
298
|
+
'sessionName',
|
|
299
|
+
];
|
|
300
|
+
export async function loadSessionSpec(spec, pluginManager) {
|
|
301
|
+
const { views, sessionAssemblies = [], sessionConnections = [], sessionTracks = [], layout, sessionName, } = spec;
|
|
125
302
|
const rootModel = pluginManager.rootModel;
|
|
126
303
|
try {
|
|
127
304
|
rootModel.setSession?.({
|
|
128
305
|
name: sessionName ?? `New session ${new Date().toLocaleString()}`,
|
|
129
306
|
});
|
|
130
307
|
const { session } = rootModel;
|
|
308
|
+
// `tracks` for sessionTracks, `assemblies` for sessionAssemblies, `name`
|
|
309
|
+
// for sessionName: a top-level key this spec does not define used to be
|
|
310
|
+
// dropped by the destructure above with nothing said, on the one door every
|
|
311
|
+
// agent-facing text names first. Reported like the misplaced keys below,
|
|
312
|
+
// rather than thrown, so the rest of the spec still loads.
|
|
313
|
+
const unknownKeys = Object.keys(spec).filter(key => !SPEC_KEYS.includes(key));
|
|
314
|
+
if (unknownKeys.length) {
|
|
315
|
+
session?.notifyError(`${unknownKeysMessage('Session spec', unknownKeys)} — it takes ${SPEC_KEYS.join(', ')}`);
|
|
316
|
+
}
|
|
131
317
|
// Assemblies first: sessionTracks and the views below reference them by
|
|
132
318
|
// name, so a self-contained spec (novel assemblies + their tracks, no
|
|
133
319
|
// hosted config) resolves only if the assemblies exist before either runs.
|
|
134
320
|
if (isSessionWithAddAssembly(session)) {
|
|
321
|
+
// Per-assembly, like connections and tracks below: an invalid config
|
|
322
|
+
// fails the array's MST type check and would otherwise cost the spec
|
|
323
|
+
// everything after it
|
|
135
324
|
for (const assembly of sessionAssemblies) {
|
|
136
|
-
|
|
325
|
+
try {
|
|
326
|
+
session.addSessionAssembly(assembly);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
console.error(e);
|
|
330
|
+
const label = typeof assembly.name === 'string' ? assembly.name : '?';
|
|
331
|
+
session.notifyError(`Assembly "${label}" has an invalid configuration: ${e}`, e);
|
|
332
|
+
}
|
|
137
333
|
}
|
|
138
334
|
}
|
|
139
335
|
// Connections after the assemblies (a connection config may name one the
|
|
@@ -180,63 +376,21 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
180
376
|
}
|
|
181
377
|
}
|
|
182
378
|
addSessionTracks(session, sessionTracks);
|
|
183
|
-
//
|
|
379
|
+
// A view type with no registered LaunchView-<type> extension point makes
|
|
184
380
|
// evaluateAsyncExtensionPoint a silent no-op, leaving an empty session with
|
|
185
|
-
// no diagnostic.
|
|
186
|
-
|
|
187
|
-
// but nothing taught it how to launch from a spec, which the spec author
|
|
188
|
-
// can't fix, only the view's plugin can.
|
|
381
|
+
// no diagnostic. Reported once per type, not per view.
|
|
382
|
+
const viewTypes = pluginManager.getElementTypeRecord('view');
|
|
189
383
|
const notLaunchable = [
|
|
190
|
-
...new Set(views.flatMap(view => pluginManager.
|
|
191
|
-
? []
|
|
192
|
-
: [view.type])),
|
|
384
|
+
...new Set(views.flatMap(view => viewTypeProblem(pluginManager, view.type) ? [view.type] : [])),
|
|
193
385
|
];
|
|
194
|
-
// the record's `has`, not getElementType, which throws on an unregistered
|
|
195
|
-
// name rather than returning undefined
|
|
196
|
-
const viewTypes = pluginManager.getElementTypeRecord('view');
|
|
197
386
|
const unknown = notLaunchable.filter(type => !viewTypes.has(type));
|
|
198
387
|
const noLauncher = notLaunchable.filter(type => viewTypes.has(type));
|
|
199
388
|
if (unknown.length) {
|
|
200
|
-
session?.notifyError(
|
|
389
|
+
session?.notifyError(unknownViewTypesMessage(unknown));
|
|
201
390
|
}
|
|
202
391
|
if (noLauncher.length) {
|
|
203
|
-
session?.notifyError(
|
|
204
|
-
}
|
|
205
|
-
// The two ways of writing a view are not the same shape, and this is the
|
|
206
|
-
// confusable pair:
|
|
207
|
-
//
|
|
208
|
-
// - a spec view is flat *arguments* to LaunchView-<type>, in that launcher's
|
|
209
|
-
// own vocabulary (a dotplot takes `views`, a spreadsheet takes `uri`), and
|
|
210
|
-
// the launcher sorts them into what the new view needs
|
|
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
|
|
213
|
-
//
|
|
214
|
-
// Reported rather than unwrapped here, deliberately: only a launcher knows
|
|
215
|
-
// which of its keys are one-shot commands versus plain view props, so
|
|
216
|
-
// flattening `init` centrally would erase that distinction and let a view prop
|
|
217
|
-
// nested inside it be honored from a spec while the identical config drops it.
|
|
218
|
-
// Teaching each launcher to accept both instead is per-view-type work that
|
|
219
|
-
// every future launcher has to remember — the shapes then differ by view type,
|
|
220
|
-
// which is worse than differing by surface. One check here covers every view
|
|
221
|
-
// type, including plugin-provided ones. Without it the LGV reports the
|
|
222
|
-
// downstream symptom, "No assembly provided", for a misplaced block.
|
|
223
|
-
const nested = views.filter(view => 'init' in view).map(view => view.type);
|
|
224
|
-
if (nested.length) {
|
|
225
|
-
session?.notifyError(`Session spec view(s) ${nested.join(', ')} nest their settings under "init". A spec takes those keys flat (assembly, loc, tracks, …); "init" is the config/defaultSession form.`);
|
|
392
|
+
session?.notifyError(noLauncherMessage(noLauncher));
|
|
226
393
|
}
|
|
227
|
-
// Launch sequentially and record the id each spec view created, so the
|
|
228
|
-
// layout below can map its indices to real views. Reading session.views
|
|
229
|
-
// positionally afterwards only works while every handler happens to addView
|
|
230
|
-
// synchronously and in order; capturing the delta per launch instead is
|
|
231
|
-
// correct even if a handler awaits or adds an auxiliary view, and lets a
|
|
232
|
-
// later spec view (e.g. a connected MsaView) reference an earlier one that
|
|
233
|
-
// now already exists. `type` is the dispatch key, not view init data:
|
|
234
|
-
// forwarding it would land in the view's declarative init blob and trip the
|
|
235
|
-
// spurious "init ignored unknown key(s): type" warning meant to catch typos.
|
|
236
|
-
// `displayName` is applied here rather than forwarded because it is a base
|
|
237
|
-
// view prop every view type has, so one path covers all of them (including
|
|
238
|
-
// plugin-provided types whose launcher never heard of it). A nested `init` is
|
|
239
|
-
// NOT unwrapped the same way — see the diagnostic above for why.
|
|
240
394
|
// Let any connection this spec registered finish before the views that
|
|
241
395
|
// reference what it supplies are launched (see whenConnectionsSettle). Gated
|
|
242
396
|
// on there being views: with none, nothing is waiting on the connection, and
|
|
@@ -250,54 +404,62 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
|
|
|
250
404
|
!(await whenConnectionsSettle(session))) {
|
|
251
405
|
return;
|
|
252
406
|
}
|
|
407
|
+
// Launch sequentially and record the ids each spec view created, so the
|
|
408
|
+
// layout below can map its indices to real views. Reading session.views
|
|
409
|
+
// positionally afterwards only works while every handler happens to addView
|
|
410
|
+
// synchronously and in order; capturing the delta per launch instead is
|
|
411
|
+
// correct even if a handler awaits or adds an auxiliary view, and lets a
|
|
412
|
+
// later spec view (e.g. a connected MsaView) reference an earlier one that
|
|
413
|
+
// now already exists.
|
|
414
|
+
//
|
|
415
|
+
// An unknown key is an ERROR rather than the snapshot path's warning: this
|
|
416
|
+
// surface's misplaced keys already report as errors, and the launcher's own
|
|
417
|
+
// failure lands as an error a line later — a warning under it reads as the
|
|
418
|
+
// lesser of the two, when it is the cause. Per view, so one bad view
|
|
419
|
+
// doesn't cost the rest.
|
|
253
420
|
const createdViewIds = [];
|
|
254
|
-
for (const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// by the plain extension-point runner, which would leave a silent empty
|
|
259
|
-
// session. A handler that legitimately no-ops returns normally and is
|
|
260
|
-
// unaffected. Per-view try/catch so one bad view doesn't abort the rest.
|
|
261
|
-
try {
|
|
262
|
-
await pluginManager.evaluateAsyncExtensionPointStrict(`LaunchView-${type}`, {
|
|
263
|
-
...view,
|
|
264
|
-
session,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
catch (e) {
|
|
268
|
-
console.error(e);
|
|
269
|
-
session?.notifyError(`Failed to launch ${type} view: ${e}`, e);
|
|
421
|
+
for (const spec of views) {
|
|
422
|
+
const { view, problem } = await launchableSpecView(pluginManager, spec);
|
|
423
|
+
if (problem) {
|
|
424
|
+
session?.notifyError(problem);
|
|
270
425
|
}
|
|
271
|
-
const created = session
|
|
272
|
-
if (
|
|
273
|
-
|
|
426
|
+
const { created, failure } = await launchSpecView(session, pluginManager, view);
|
|
427
|
+
if (failure) {
|
|
428
|
+
session?.notifyError(failure.message, failure.cause);
|
|
274
429
|
}
|
|
275
|
-
createdViewIds.push(created
|
|
430
|
+
createdViewIds.push(created.map(v => v.id));
|
|
276
431
|
}
|
|
277
432
|
if (layout && session) {
|
|
278
|
-
const
|
|
433
|
+
const knownIds = new Set(createdViewIds.flat());
|
|
434
|
+
const bad = unresolvedLayoutRefs(layout, views.length, knownIds);
|
|
435
|
+
const badIndices = bad.filter(ref => typeof ref === 'number');
|
|
436
|
+
const badIds = bad.filter(ref => typeof ref === 'string');
|
|
279
437
|
if (badIndices.length) {
|
|
280
438
|
session.notifyError(`Session spec layout references view index ${badIndices.join(', ')}, but the spec has ${views.length} view(s).`);
|
|
281
439
|
}
|
|
440
|
+
if (badIds.length) {
|
|
441
|
+
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).`);
|
|
442
|
+
}
|
|
282
443
|
if (unsizeableLayoutNodes(layout)) {
|
|
283
444
|
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
445
|
}
|
|
285
446
|
if (flattenedTabsContainers(layout)) {
|
|
286
447
|
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
448
|
}
|
|
288
|
-
if (
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
// in
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
449
|
+
if (hasLayoutViews(session)) {
|
|
450
|
+
// Its own try/catch, the same reasoning as the per-view one above: the
|
|
451
|
+
// resolver throws for a layout it will not arrange, and this is the
|
|
452
|
+
// LAST thing the spec does — so an unarrangeable layout used to take
|
|
453
|
+
// the whole load's catch and report itself as the session's error. The
|
|
454
|
+
// layout is one statement in the spec; losing it should cost the spec
|
|
455
|
+
// its layout and nothing else.
|
|
456
|
+
try {
|
|
457
|
+
session.layoutViews(convertLayoutNode(layout, createdViewIds, knownIds, new Set(pinnedLayoutIds(layout, knownIds))));
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
console.error(e);
|
|
461
|
+
session.notifyError(`Session spec layout: ${e}`, e);
|
|
462
|
+
}
|
|
301
463
|
}
|
|
302
464
|
else {
|
|
303
465
|
session.notifyError('Session spec has a "layout", but this application does not support workspace layouts');
|