@jbrowse/app-core 4.3.0 → 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/AppSession/AppRootModel.d.ts +30 -0
- package/esm/AppSession/AppRootModel.js +1 -0
- package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
- package/esm/AppSession/AppSessionMixin.js +62 -0
- package/esm/AppSession/index.d.ts +2 -0
- package/esm/AppSession/index.js +1 -0
- package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
- package/esm/Assemblies/AssembliesMixin.js +149 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +19 -0
- package/esm/HistoryManagement/index.js +42 -5
- package/esm/JBrowseConfig/RootConfiguration.d.ts +171 -78
- package/esm/JBrowseConfig/RootConfiguration.js +85 -7
- package/esm/JBrowseConfig/index.d.ts +173 -84
- package/esm/JBrowseConfig/index.js +62 -2
- package/esm/JBrowseModel/index.d.ts +180 -95
- package/esm/JBrowseModel/index.js +122 -21
- 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 +69 -3
- package/esm/RootMenu/index.js +96 -61
- package/esm/SessionSpec/index.d.ts +6 -0
- package/esm/SessionSpec/index.js +3 -0
- package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
- package/esm/SessionSpec/lgvUrlInit.js +158 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
- package/esm/SessionSpec/loadSessionSpec.js +392 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
- package/esm/SessionSpec/types.d.ts +31 -0
- package/esm/SessionSpec/types.js +1 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
- package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
- package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
- package/esm/WorkspaceLayout/PanelView.js +99 -0
- package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
- package/esm/WorkspaceLayout/TabStrip.js +226 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
- package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
- package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
- package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
- package/esm/WorkspaceLayout/dropZone.js +113 -0
- package/esm/WorkspaceLayout/index.d.ts +8 -0
- package/esm/WorkspaceLayout/index.js +4 -0
- package/esm/WorkspaceLayout/model.d.ts +248 -0
- package/esm/WorkspaceLayout/model.js +562 -0
- package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
- package/esm/WorkspaceLayout/panelChrome.js +4 -0
- package/esm/WorkspaceLayout/spec.d.ts +88 -0
- package/esm/WorkspaceLayout/spec.js +226 -0
- package/esm/WorkspaceLayout/splitter.d.ts +25 -0
- package/esm/WorkspaceLayout/splitter.js +37 -0
- package/esm/WorkspaceLayout/tree.d.ts +158 -0
- package/esm/WorkspaceLayout/tree.js +379 -0
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
- package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
- package/esm/index.d.ts +18 -9
- package/esm/index.js +16 -9
- package/esm/menus.d.ts +55 -68
- package/esm/menus.js +117 -86
- package/esm/queryParams.d.ts +9 -0
- package/esm/queryParams.js +92 -0
- package/esm/sessionModelFactory.d.ts +18 -0
- package/esm/sessionModelFactory.js +1 -0
- package/esm/ui/App/App.d.ts +3 -14
- package/esm/ui/App/App.js +30 -17
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppReadyMarker.d.ts +43 -0
- package/esm/ui/App/AppReadyMarker.js +56 -0
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +23 -24
- package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
- package/esm/ui/App/ClassicViewsContainer.js +9 -8
- package/esm/ui/App/DialogQueue.d.ts +17 -1
- package/esm/ui/App/DialogQueue.js +30 -1
- package/esm/ui/App/Drawer.d.ts +1 -1
- package/esm/ui/App/Drawer.js +10 -39
- package/esm/ui/App/DrawerControls.d.ts +1 -5
- package/esm/ui/App/DrawerControls.js +1 -25
- package/esm/ui/App/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -10
- package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
- package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
- package/esm/ui/App/DrawerWidget.d.ts +1 -1
- package/esm/ui/App/DrawerWidget.js +9 -21
- package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
- package/esm/ui/App/JBrowseTabMenu.js +24 -5
- package/esm/ui/App/ViewButtons.d.ts +2 -4
- package/esm/ui/App/ViewButtons.js +15 -3
- package/esm/ui/App/ViewContainer.d.ts +3 -2
- package/esm/ui/App/ViewContainer.js +144 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +9 -2
- package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
- package/esm/ui/App/ViewErrorFallback.js +42 -0
- package/esm/ui/App/ViewHeader.d.ts +3 -4
- package/esm/ui/App/ViewHeader.js +28 -17
- package/esm/ui/App/ViewLauncher.d.ts +2 -8
- package/esm/ui/App/ViewLauncher.js +7 -5
- package/esm/ui/App/ViewMenu.d.ts +3 -5
- package/esm/ui/App/ViewMenu.js +87 -92
- package/esm/ui/App/ViewStack.d.ts +7 -0
- package/esm/ui/App/ViewStack.js +42 -0
- package/esm/ui/App/ViewWrapper.d.ts +15 -1
- package/esm/ui/App/ViewWrapper.js +26 -10
- package/esm/ui/App/ViewsContainer.d.ts +3 -8
- package/esm/ui/App/ViewsContainer.js +9 -6
- package/esm/ui/App/copyView.js +1 -1
- package/esm/ui/App/index.d.ts +2 -1
- package/esm/ui/App/index.js +3 -2
- package/esm/ui/App/lazyChunk.d.ts +13 -0
- package/esm/ui/App/lazyChunk.js +91 -0
- package/esm/ui/App/types.d.ts +12 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +34 -0
- package/esm/ui/App/useViewVisibility.js +87 -0
- package/esm/ui/App/viewTitle.d.ts +5 -0
- package/esm/ui/App/viewTitle.js +9 -0
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +12 -17
- package/esm/AppFocus/index.d.ts +0 -5
- package/esm/AppFocus/index.js +0 -12
- package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
- package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
- package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
- package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
- package/esm/DockviewLayout/index.d.ts +0 -35
- package/esm/DockviewLayout/index.js +0 -111
- package/esm/ui/App/DockviewContext.d.ts +0 -22
- package/esm/ui/App/DockviewContext.js +0 -29
- package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
- package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
- package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
- package/esm/ui/App/DrawerWidgetSelector.js +0 -47
- package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
- package/esm/ui/App/JBrowseViewPanel.js +0 -40
- package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
- package/esm/ui/App/JBrowseViewTab.js +0 -97
- package/esm/ui/App/ModalWidget.d.ts +0 -6
- package/esm/ui/App/ModalWidget.js +0 -48
- package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
- package/esm/ui/App/TiledViewsContainer.js +0 -366
- package/esm/ui/App/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { isSessionWithAddAssembly, isSessionWithAddSessionTrack, } from '@jbrowse/core/util';
|
|
2
|
+
import { legacyInitMessage, unknownKeysMessage, } from '@jbrowse/core/util/withLaunchInput';
|
|
3
|
+
import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
4
|
+
import { when } from 'mobx';
|
|
5
|
+
function isSessionWithWorkspaceLayout(session) {
|
|
6
|
+
return ('applyLayoutSpec' in session &&
|
|
7
|
+
'setUseWorkspaces' in session &&
|
|
8
|
+
'orderViews' in session);
|
|
9
|
+
}
|
|
10
|
+
function isSessionWithSpecConnections(session) {
|
|
11
|
+
return ('addConnectionConf' in session &&
|
|
12
|
+
'makeConnection' in session &&
|
|
13
|
+
'connectionInstances' in session);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Register `&sessionTracks=` / a spec's `sessionTracks` into a session.
|
|
17
|
+
*
|
|
18
|
+
* Exported because the hub launch needs it too and must not reimplement it.
|
|
19
|
+
*
|
|
20
|
+
* A track that fails to register does not cost the caller the rest of them —
|
|
21
|
+
* same reasoning as the per-connection and per-view try/catch around it, one
|
|
22
|
+
* layer down.
|
|
23
|
+
*/
|
|
24
|
+
export function addSessionTracks(session, tracks) {
|
|
25
|
+
if (!tracks.length) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!isSessionWithAddSessionTrack(session)) {
|
|
29
|
+
session?.notifyError('This link has "sessionTracks", but this application cannot add tracks to a session');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const track of tracks) {
|
|
33
|
+
const label = typeof track.trackId === 'string' ? track.trackId : '?';
|
|
34
|
+
try {
|
|
35
|
+
session.addSessionTrackConf(track);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.error(e);
|
|
39
|
+
session.notifyError(`Track "${label}" has an invalid configuration: ${e}`, e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// A connection supplies assemblies and tracks that the spec's views go on to
|
|
44
|
+
// reference by name, and it supplies them from a fetch. Launching a view before
|
|
45
|
+
// that fetch lands gives it an assembly that doesn't exist yet ("Assembly X not
|
|
46
|
+
// found") and a `tracks` list whose ids don't resolve yet.
|
|
47
|
+
//
|
|
48
|
+
// Causal, not timed: every connection reports `loading` from its own
|
|
49
|
+
// afterAttach until its connect() promise settles, success or failure, so this
|
|
50
|
+
// proceeds the moment the last one is done and never waits on one that failed.
|
|
51
|
+
// A session destroyed mid-wait (a StrictMode remount, a session swap) resolves
|
|
52
|
+
// it too — reading a detached node throws, and here that would land in the
|
|
53
|
+
// caller's catch and report a spurious error against the *new* session.
|
|
54
|
+
async function whenConnectionsSettle(session) {
|
|
55
|
+
// isStateTreeNode first: the liveness question only applies to a real MST
|
|
56
|
+
// session, and this is called with plain object sessions under test
|
|
57
|
+
const gone = () => isStateTreeNode(session) && !isAlive(session);
|
|
58
|
+
await when(() => gone() || session.connectionInstances.every(conn => !conn.loading));
|
|
59
|
+
return !gone();
|
|
60
|
+
}
|
|
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
|
+
];
|
|
110
|
+
}
|
|
111
|
+
// A layout index is a position in the spec's `views` array, so one past the end
|
|
112
|
+
// (or negative) is an authoring slip that would otherwise silently drop that
|
|
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.
|
|
121
|
+
return [
|
|
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
|
+
: []),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
// A `tabs` node shares one cell between its children rather than dividing the
|
|
133
|
+
// space, so a `size` on one of them describes nothing. This is the ONLY case
|
|
134
|
+
// left where a stated size is dropped.
|
|
135
|
+
//
|
|
136
|
+
// It used to be far wider — dockview forces its branches to alternate
|
|
137
|
+
// orientation by depth, so a nested container had no branch to size against and
|
|
138
|
+
// the whole sizing pass bailed, top-level numbers included. That is gone
|
|
139
|
+
// (ADR-068): the spec's nesting is the tree's nesting, `size` applies wherever
|
|
140
|
+
// it is written, and a bare sibling takes an equal share of the remainder. The
|
|
141
|
+
// wider check outlived the limitation it reported and told an author their
|
|
142
|
+
// nested or partial sizes had been ignored while the layout honoured them —
|
|
143
|
+
// which is also what website/docs/urlparams.md promises.
|
|
144
|
+
function unsizeableLayoutNodes(layout) {
|
|
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);
|
|
148
|
+
}
|
|
149
|
+
// The other statement a `tabs` node cannot take literally, and the one that
|
|
150
|
+
// used to fail silently rather than partially: a tab holds a FLAT stack of
|
|
151
|
+
// views, so a container child has no split to become. Containers nest
|
|
152
|
+
// arbitrarily deep everywhere else — the docs say so — which is what makes one
|
|
153
|
+
// here easy to write. Its views are flattened into a single tab now; they used
|
|
154
|
+
// to be dropped from the layout, after which homing swept them into whichever
|
|
155
|
+
// tab happened to be showing, so the arrangement was wrong with nothing said.
|
|
156
|
+
function flattenedTabsContainers(layout) {
|
|
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);
|
|
160
|
+
}
|
|
161
|
+
// use extension point named e.g. LaunchView-LinearGenomeView to initialize an
|
|
162
|
+
// LGV session
|
|
163
|
+
export async function loadSessionSpec({ views, sessionAssemblies = [], sessionConnections = [], sessionTracks = [], layout, sessionName, }, pluginManager) {
|
|
164
|
+
const rootModel = pluginManager.rootModel;
|
|
165
|
+
try {
|
|
166
|
+
rootModel.setSession?.({
|
|
167
|
+
name: sessionName ?? `New session ${new Date().toLocaleString()}`,
|
|
168
|
+
});
|
|
169
|
+
const { session } = rootModel;
|
|
170
|
+
// Assemblies first: sessionTracks and the views below reference them by
|
|
171
|
+
// name, so a self-contained spec (novel assemblies + their tracks, no
|
|
172
|
+
// hosted config) resolves only if the assemblies exist before either runs.
|
|
173
|
+
if (isSessionWithAddAssembly(session)) {
|
|
174
|
+
for (const assembly of sessionAssemblies) {
|
|
175
|
+
session.addSessionAssembly(assembly);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Connections after the assemblies (a connection config may name one the
|
|
179
|
+
// spec defines) and before the tracks and views, which are what reference
|
|
180
|
+
// the assemblies and tracks a connection brings in.
|
|
181
|
+
if (sessionConnections.length) {
|
|
182
|
+
if (session && isSessionWithSpecConnections(session)) {
|
|
183
|
+
// A connection type nothing registered is a typo or a missing plugin,
|
|
184
|
+
// the same authoring slip the view check below reports. Caught here
|
|
185
|
+
// because the config would otherwise fail the connection array's MST
|
|
186
|
+
// union check and throw past every per-key guard to the catch at the
|
|
187
|
+
// bottom, costing the spec its tracks, its views and its layout over
|
|
188
|
+
// one bad entry — and reporting it as a raw union-type dump.
|
|
189
|
+
const connectionTypes = pluginManager.getElementTypeRecord('connection');
|
|
190
|
+
for (const conf of sessionConnections) {
|
|
191
|
+
const { type } = conf;
|
|
192
|
+
const label = typeof conf.connectionId === 'string' ? conf.connectionId : '?';
|
|
193
|
+
if (typeof type !== 'string' || !connectionTypes.has(type)) {
|
|
194
|
+
session.notifyError(`Session spec connection "${label}" has ${typeof type === 'string'
|
|
195
|
+
? `unknown type "${type}". The plugin providing the connection may be missing, or the type may be misspelled.`
|
|
196
|
+
: 'no "type".'}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// Per-connection, for the same reason each view gets its own: one
|
|
200
|
+
// unusable connection shouldn't cost the spec everything after it.
|
|
201
|
+
try {
|
|
202
|
+
// silent whenever the spec launches views of its own: a connection
|
|
203
|
+
// that opens one on connect (a single-file UCSC hub goes to its
|
|
204
|
+
// `defaultPos`) would otherwise open a second view competing with
|
|
205
|
+
// the one the spec asked for. With no views the spec is only asking
|
|
206
|
+
// to attach the connection, so let it do its own thing.
|
|
207
|
+
session.makeConnection(session.addSessionConnectionConf
|
|
208
|
+
? session.addSessionConnectionConf(conf)
|
|
209
|
+
: session.addConnectionConf(conf), { silent: views.length > 0 });
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
console.error(e);
|
|
213
|
+
session.notifyError(`Connection "${label}" has an invalid configuration: ${e}`, e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
session?.notifyError('Session spec has "sessionConnections", but this application cannot add connections to a session');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
addSessionTracks(session, sessionTracks);
|
|
222
|
+
// a view type with no registered LaunchView-<type> extension point makes
|
|
223
|
+
// evaluateAsyncExtensionPoint a silent no-op, leaving an empty session with
|
|
224
|
+
// no diagnostic. Two different causes, so two messages: the view type is
|
|
225
|
+
// unknown here at all (a typo, or a plugin that wasn't loaded), or it exists
|
|
226
|
+
// but nothing taught it how to launch from a spec, which the spec author
|
|
227
|
+
// can't fix, only the view's plugin can.
|
|
228
|
+
const notLaunchable = [
|
|
229
|
+
...new Set(views.flatMap(view => pluginManager.extensionPoints.has(`LaunchView-${view.type}`)
|
|
230
|
+
? []
|
|
231
|
+
: [view.type])),
|
|
232
|
+
];
|
|
233
|
+
// the record's `has`, not getElementType, which throws on an unregistered
|
|
234
|
+
// name rather than returning undefined
|
|
235
|
+
const viewTypes = pluginManager.getElementTypeRecord('view');
|
|
236
|
+
const unknown = notLaunchable.filter(type => !viewTypes.has(type));
|
|
237
|
+
const noLauncher = notLaunchable.filter(type => viewTypes.has(type));
|
|
238
|
+
if (unknown.length) {
|
|
239
|
+
session?.notifyError(`Unknown view type(s) in session spec: ${unknown.join(', ')}. The plugin providing the view may be missing, or the type may be misspelled.`);
|
|
240
|
+
}
|
|
241
|
+
if (noLauncher.length) {
|
|
242
|
+
session?.notifyError(`View type(s) ${noLauncher.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`);
|
|
243
|
+
}
|
|
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.
|
|
264
|
+
//
|
|
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.
|
|
268
|
+
//
|
|
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
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Launch sequentially and record the id each spec view created, so the
|
|
287
|
+
// layout below can map its indices to real views. Reading session.views
|
|
288
|
+
// positionally afterwards only works while every handler happens to addView
|
|
289
|
+
// synchronously and in order; capturing the delta per launch instead is
|
|
290
|
+
// correct even if a handler awaits or adds an auxiliary view, and lets a
|
|
291
|
+
// later spec view (e.g. a connected MsaView) reference an earlier one that
|
|
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.
|
|
295
|
+
// `displayName` is applied here rather than forwarded because it is a base
|
|
296
|
+
// view prop every view type has, so one path covers all of them (including
|
|
297
|
+
// plugin-provided types whose launcher never heard of it).
|
|
298
|
+
// Let any connection this spec registered finish before the views that
|
|
299
|
+
// reference what it supplies are launched (see whenConnectionsSettle). Gated
|
|
300
|
+
// on there being views: with none, nothing is waiting on the connection, and
|
|
301
|
+
// holding the load open until a hub finishes fetching would be pure delay —
|
|
302
|
+
// the connection is the deliverable. A session that went away while we
|
|
303
|
+
// waited has nothing left to launch into.
|
|
304
|
+
if (sessionConnections.length &&
|
|
305
|
+
views.length &&
|
|
306
|
+
session &&
|
|
307
|
+
isSessionWithSpecConnections(session) &&
|
|
308
|
+
!(await whenConnectionsSettle(session))) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const createdViewIds = [];
|
|
312
|
+
for (const { type, displayName, ...view } of specViews) {
|
|
313
|
+
const before = new Set(session?.views.map(v => v.id));
|
|
314
|
+
// Strict so a launch handler that throws (missing/invalid assembly,
|
|
315
|
+
// unresolved track, ...) surfaces as a snackbar instead of being swallowed
|
|
316
|
+
// by the plain extension-point runner, which would leave a silent empty
|
|
317
|
+
// session. A handler that legitimately no-ops returns normally and is
|
|
318
|
+
// unaffected. Per-view try/catch so one bad view doesn't abort the rest.
|
|
319
|
+
try {
|
|
320
|
+
await pluginManager.evaluateAsyncExtensionPointStrict(`LaunchView-${type}`, {
|
|
321
|
+
...view,
|
|
322
|
+
session,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (e) {
|
|
326
|
+
console.error(e);
|
|
327
|
+
session?.notifyError(`Failed to launch ${type} view: ${e}`, e);
|
|
328
|
+
}
|
|
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);
|
|
336
|
+
}
|
|
337
|
+
createdViewIds.push(created.map(v => v.id));
|
|
338
|
+
}
|
|
339
|
+
if (layout && session) {
|
|
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');
|
|
344
|
+
if (badIndices.length) {
|
|
345
|
+
session.notifyError(`Session spec layout references view index ${badIndices.join(', ')}, but the spec has ${views.length} view(s).`);
|
|
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
|
+
}
|
|
350
|
+
if (unsizeableLayoutNodes(layout)) {
|
|
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');
|
|
352
|
+
}
|
|
353
|
+
if (flattenedTabsContainers(layout)) {
|
|
354
|
+
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');
|
|
355
|
+
}
|
|
356
|
+
if (isSessionWithWorkspaceLayout(session)) {
|
|
357
|
+
// Enable workspaces mode for this session only — a spec URL shouldn't
|
|
358
|
+
// rewrite the visitor's own preference
|
|
359
|
+
session.setUseWorkspaces(true);
|
|
360
|
+
// A tab's `viewIds` is membership, not order: a tab renders its views
|
|
361
|
+
// in `session.views` order (WorkspaceContainer's `viewsOf`, and
|
|
362
|
+
// `viewIdsForTab`). So the top-to-bottom order a spec panel states —
|
|
363
|
+
// `viewIds` is documented as "the views to stack vertically in one tab"
|
|
364
|
+
// — only takes effect if it is applied to `session.views`, which is
|
|
365
|
+
// what `applyLayoutSpec`'s return value is for. Drop this call and the
|
|
366
|
+
// stated order is silently ignored: the tree holds it, nothing reads
|
|
367
|
+
// it, and the views come back in launch order with no diagnostic.
|
|
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
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
session.notifyError('Session spec has a "layout", but this application does not support workspace layouts');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
console.error(e);
|
|
390
|
+
rootModel.session?.notifyError(`${e}`, e);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LayoutNode, ViewSpec } from './types.ts';
|
|
2
|
+
export interface ParsedSessionSpec {
|
|
3
|
+
configUrl?: string;
|
|
4
|
+
spec: {
|
|
5
|
+
views: ViewSpec[];
|
|
6
|
+
sessionAssemblies?: Record<string, unknown>[];
|
|
7
|
+
sessionConnections?: Record<string, unknown>[];
|
|
8
|
+
sessionTracks?: Record<string, unknown>[];
|
|
9
|
+
layout?: LayoutNode;
|
|
10
|
+
};
|
|
11
|
+
sessionName?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseSessionSpecUrl(input: string): ParsedSessionSpec;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { buildLgvInitFromParams, hubConnectionSpec, readHubUrlParam, } from './lgvUrlInit.js';
|
|
2
|
+
// jbrowse-web keeps its URL params in the hash fragment XOR the query string
|
|
3
|
+
// (the inline `encoded-`/`json-`/`spec-` sessions live in the hash to dodge the
|
|
4
|
+
// server request-line limit; short `share-` links stay in the query). Read from
|
|
5
|
+
// whichever the link uses so a pasted hash-form link parses the same as a
|
|
6
|
+
// query-form one, instead of tripping the "no session in it" error below.
|
|
7
|
+
//
|
|
8
|
+
// This MUST stay identical to jbrowse-web's own reader (`useQueryParam.ts`
|
|
9
|
+
// `paramLocation`: `hash.includes('=') ? hash : search`) — that module's whole
|
|
10
|
+
// point, and this file's, is that a URL resolves the same everywhere. The XOR is
|
|
11
|
+
// a documented invariant its producers (`buildShareUrl`) guarantee, so a
|
|
12
|
+
// both-present link is a non-case; don't "harden" this into a merge that would
|
|
13
|
+
// diverge from the canonical parser.
|
|
14
|
+
function linkParams(url) {
|
|
15
|
+
const hash = url.hash.slice(1);
|
|
16
|
+
return new URLSearchParams(hash.includes('=') ? hash : url.search);
|
|
17
|
+
}
|
|
18
|
+
// A `jbrowse://open?url=<web link>` wrapper carries a JBrowse Web url for
|
|
19
|
+
// Desktop's protocol handler to unwrap. Every page that offers an "Open in
|
|
20
|
+
// Desktop" button puts that link right next to the plain web one — the docs'
|
|
21
|
+
// figure dialogs, genomes.jbrowse.org — so the link someone copies and pastes
|
|
22
|
+
// into "Open JBrowse Web link..." is as often this as the url inside it. It
|
|
23
|
+
// parses as a perfectly good URL whose query has no `session`, which reported
|
|
24
|
+
// "that link has no session in it" about a link that does. Unwrap it instead.
|
|
25
|
+
//
|
|
26
|
+
// Restricted to http(s), and to a single unwrap, for the reason Desktop's own
|
|
27
|
+
// parser is (products/jbrowse-desktop/electron/launchTarget.ts, which gates the
|
|
28
|
+
// OS-delivered copy of this and cannot import from here — it is bundled into the
|
|
29
|
+
// Electron main process): the payload comes from whatever handed us the link.
|
|
30
|
+
function unwrapProtocolUrl(url) {
|
|
31
|
+
if (url.protocol !== 'jbrowse:') {
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
const wrapped = url.searchParams.get('url');
|
|
35
|
+
if (!wrapped) {
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const inner = new URL(wrapped);
|
|
40
|
+
return inner.protocol === 'http:' || inner.protocol === 'https:'
|
|
41
|
+
? inner
|
|
42
|
+
: url;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolveConfig(params, url) {
|
|
49
|
+
const config = params.get('config');
|
|
50
|
+
return {
|
|
51
|
+
// a relative config (e.g. `test_data/volvox/config.json`) is served by the
|
|
52
|
+
// instance the link points at, so resolve it there rather than handing back
|
|
53
|
+
// a path nothing outside that instance could fetch.
|
|
54
|
+
//
|
|
55
|
+
// `config=none` is jbrowse-web's "load no config at all" sentinel
|
|
56
|
+
// (SessionLoader.fetchConfig), and the form a self-contained spec link has to
|
|
57
|
+
// take — omitting `config` entirely makes web fall back to its own
|
|
58
|
+
// config.json. Treated as no config here, not resolved, which would hand the
|
|
59
|
+
// caller `<instance>/none` to fetch.
|
|
60
|
+
configUrl: config && config !== 'none' ? new URL(config, url.href).href : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The spec a link with no `session=` describes — or the error explaining why it
|
|
65
|
+
* describes none.
|
|
66
|
+
*
|
|
67
|
+
* The branches below are in jbrowse-web's own ranking (SessionLoader's
|
|
68
|
+
* loadSessionByType), and have to be, because a link can carry several of these
|
|
69
|
+
* at once and which one wins decides what the link means:
|
|
70
|
+
*
|
|
71
|
+
* 1. `&extendSession=true` alongside the shorthand, which outranks even a hub.
|
|
72
|
+
* 2. `&hubURL=`, above the bare shorthand, because a hub is the only param that
|
|
73
|
+
* brings its own assemblies and tracks — a link carrying both is asking to
|
|
74
|
+
* navigate *inside* the hub. Ranking the shorthand first builds a bare LGV
|
|
75
|
+
* and drops the hub with no diagnostic.
|
|
76
|
+
* 3. the shorthand on its own.
|
|
77
|
+
*/
|
|
78
|
+
function shorthandSpec(params) {
|
|
79
|
+
const init = buildLgvInitFromParams(params);
|
|
80
|
+
// what makes a link a jb1-style one, the same test as jbrowse-web's
|
|
81
|
+
// isJb1StyleSession. The rest of the shorthand only modifies a view; none of
|
|
82
|
+
// it can conjure one.
|
|
83
|
+
const isJb1Style = init.loc !== undefined || init.assembly !== undefined;
|
|
84
|
+
// `&extendSession=true` means "apply this shorthand to the config's own
|
|
85
|
+
// defaultSession", and there is no honest way to do that here: this builds a
|
|
86
|
+
// session spec, and loadSessionSpec replaces the session rather than
|
|
87
|
+
// navigating one the config supplied. Opening the view without the
|
|
88
|
+
// defaultSession's curated tracks would quietly be a different session than
|
|
89
|
+
// the link asks for, so say so instead — and say what to remove, since the
|
|
90
|
+
// link works fine without it.
|
|
91
|
+
//
|
|
92
|
+
// Gated on the shorthand being present for the same reason web's branch is:
|
|
93
|
+
// the param names the default session as the thing to navigate, and with no
|
|
94
|
+
// loc or assembly there is no navigation to apply to it.
|
|
95
|
+
if (isJb1Style && params.get('extendSession') === 'true') {
|
|
96
|
+
throw new Error('That link uses "&extendSession=true", which layers its location onto a defaultSession that only the JBrowse Web instance it points at can supply. Remove that parameter to open just the view the link describes.');
|
|
97
|
+
}
|
|
98
|
+
const hubURL = readHubUrlParam(params.get('hubURL') ?? undefined);
|
|
99
|
+
if (hubURL.length) {
|
|
100
|
+
// A track hub is a session connection, which a spec has carried since
|
|
101
|
+
// sessionConnections landed. So this is no longer the dead end it was
|
|
102
|
+
// ("that's a track hub, go add it by hand") — the hub attaches, and
|
|
103
|
+
// loadSessionSpec waits for it to settle before launching anything, because
|
|
104
|
+
// the assemblies and tracks the view below names are ones the hub brings.
|
|
105
|
+
return {
|
|
106
|
+
sessionConnections: hubURL.map(hubConnectionSpec),
|
|
107
|
+
sessionTracks: readSessionTracks(params),
|
|
108
|
+
// Only with an assembly. The launcher resolves everything against one,
|
|
109
|
+
// and a hub's assemblies are genome ids only the hub carries, so `&loc=`
|
|
110
|
+
// alone has nothing to launch against. Leaving the list empty is what
|
|
111
|
+
// makes loadSessionSpec register the connection non-silently, so the hub
|
|
112
|
+
// opens at its own defaultPos — the best guess available without a name,
|
|
113
|
+
// and what jbrowse-web does with the same link.
|
|
114
|
+
views: init.assembly === undefined ? [] : [lgvSpec(init)],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (!isJb1Style) {
|
|
118
|
+
throw new Error('That link has no session in it. Copy a JBrowse Web link that contains "&session=spec-...", or one with "&assembly=" and "&loc=".');
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
// `&sessionTracks=` carries whole track configs the views then reference by
|
|
122
|
+
// id, exactly as in a spec link
|
|
123
|
+
sessionTracks: readSessionTracks(params),
|
|
124
|
+
views: [lgvSpec(init)],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// The init keys are LGV `InitState` ones the LaunchView-LinearGenomeView
|
|
128
|
+
// extension point partitions for itself; ViewSpec carries only what every view
|
|
129
|
+
// type has, which is why this spreads rather than naming them.
|
|
130
|
+
function lgvSpec(init) {
|
|
131
|
+
return { type: 'LinearGenomeView', ...init };
|
|
132
|
+
}
|
|
133
|
+
function readSessionTracks(params) {
|
|
134
|
+
const sessionTracks = params.get('sessionTracks');
|
|
135
|
+
if (!sessionTracks) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(sessionTracks);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
throw new Error(`The "sessionTracks" in that link isn't valid JSON: ${e}`, {
|
|
144
|
+
cause: e,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(parsed)) {
|
|
148
|
+
throw new Error('The "sessionTracks" in that link is not a list of track configurations.');
|
|
149
|
+
}
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
export function parseSessionSpecUrl(input) {
|
|
153
|
+
let url;
|
|
154
|
+
try {
|
|
155
|
+
url = unwrapProtocolUrl(new URL(input.trim()));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw new Error(`Not a URL: ${input}`);
|
|
159
|
+
}
|
|
160
|
+
const params = linkParams(url);
|
|
161
|
+
const session = params.get('session');
|
|
162
|
+
if (!session) {
|
|
163
|
+
return {
|
|
164
|
+
...resolveConfig(params, url),
|
|
165
|
+
spec: shorthandSpec(params),
|
|
166
|
+
sessionName: params.get('sessionName') ?? undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (!session.startsWith('spec-')) {
|
|
170
|
+
const kind = /^(share|encoded|local|json)-/.exec(session)?.[1];
|
|
171
|
+
throw new Error(kind
|
|
172
|
+
? `This is a "${kind}" session link, which only the JBrowse Web instance that created it can open. Session-spec links (&session=spec-...) work here.`
|
|
173
|
+
: 'Unrecognized session in that link; only session-spec links (&session=spec-...) can be opened here.');
|
|
174
|
+
}
|
|
175
|
+
let spec;
|
|
176
|
+
try {
|
|
177
|
+
spec = JSON.parse(session.slice('spec-'.length));
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
throw new Error(`The session spec in that link isn't valid JSON: ${e}`, {
|
|
181
|
+
cause: e,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// `views: []` is legitimate and deliberate — it opens a session showing the
|
|
185
|
+
// import form (several docs figures are exactly that). Only a spec with no
|
|
186
|
+
// views *key* is malformed, and would otherwise build an empty session that
|
|
187
|
+
// reads as a silent failure.
|
|
188
|
+
if (!Array.isArray(spec.views)) {
|
|
189
|
+
throw new Error('The session spec in that link has no "views" list, so there is nothing to open.');
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
...resolveConfig(params, url),
|
|
193
|
+
spec,
|
|
194
|
+
sessionName: params.get('sessionName') ?? undefined,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LayoutSpecNode } from '../WorkspaceLayout/spec.ts';
|
|
2
|
+
import type { TrackInit } from '@jbrowse/core/util/tracks';
|
|
3
|
+
export interface ViewSpec {
|
|
4
|
+
type: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
tracks?: TrackInit[];
|
|
8
|
+
assembly?: string;
|
|
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>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
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`.
|
|
20
|
+
*
|
|
21
|
+
* ```json
|
|
22
|
+
* {
|
|
23
|
+
* "direction": "horizontal",
|
|
24
|
+
* "children": [
|
|
25
|
+
* { "views": [0, 1], "size": 70 },
|
|
26
|
+
* { "views": [2], "size": 30 }
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export type LayoutNode = LayoutSpecNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { WorkspaceLayout } from './model.ts';
|
|
2
|
+
import type { PanelChrome } from './panelChrome.ts';
|
|
3
|
+
import type { LayoutTree } from './tree.ts';
|
|
4
|
+
import type { DragState } from './useLayoutDrag.ts';
|
|
5
|
+
/**
|
|
6
|
+
* The layout, rendered. There is no imperative api and no event to listen to:
|
|
7
|
+
* the tree is state, this is a function of it, and a gesture is an action.
|
|
8
|
+
*
|
|
9
|
+
* Sizes are `flex-grow`, which is why there is no resize handling anywhere in
|
|
10
|
+
* here. A branch's children divide its space in proportion to their `size`
|
|
11
|
+
* whatever that space becomes, so a window resize is the browser's problem —
|
|
12
|
+
* this is the layout maths dockview does in pixels, and it is the part of a
|
|
13
|
+
* grid engine most likely to be got subtly wrong.
|
|
14
|
+
*/
|
|
15
|
+
interface Props {
|
|
16
|
+
node: LayoutTree;
|
|
17
|
+
layout: WorkspaceLayout;
|
|
18
|
+
/** the app's half of a panel, forwarded unchanged all the way down */
|
|
19
|
+
chrome: PanelChrome;
|
|
20
|
+
/** the in-flight drag, so the cell under the pointer can show where it lands */
|
|
21
|
+
drag?: DragState;
|
|
22
|
+
}
|
|
23
|
+
export declare const LayoutRenderer: (props: Props) => import("react").JSX.Element;
|
|
24
|
+
export {};
|