@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.
Files changed (158) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
  8. package/esm/Assemblies/AssembliesMixin.js +149 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +19 -0
  12. package/esm/HistoryManagement/index.js +42 -5
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +171 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +85 -7
  15. package/esm/JBrowseConfig/index.d.ts +173 -84
  16. package/esm/JBrowseConfig/index.js +62 -2
  17. package/esm/JBrowseModel/index.d.ts +180 -95
  18. package/esm/JBrowseModel/index.js +122 -21
  19. package/esm/JbApi/index.d.ts +2 -0
  20. package/esm/JbApi/index.js +1 -0
  21. package/esm/JbApi/jbApi.d.ts +325 -0
  22. package/esm/JbApi/jbApi.js +748 -0
  23. package/esm/RootMenu/index.d.ts +69 -3
  24. package/esm/RootMenu/index.js +96 -61
  25. package/esm/SessionSpec/index.d.ts +6 -0
  26. package/esm/SessionSpec/index.js +3 -0
  27. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  28. package/esm/SessionSpec/lgvUrlInit.js +158 -0
  29. package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
  30. package/esm/SessionSpec/loadSessionSpec.js +392 -0
  31. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
  32. package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
  33. package/esm/SessionSpec/types.d.ts +31 -0
  34. package/esm/SessionSpec/types.js +1 -0
  35. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  36. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  37. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  38. package/esm/WorkspaceLayout/PanelView.js +99 -0
  39. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  40. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  41. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  42. package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
  43. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  44. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  45. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  46. package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
  47. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  48. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  49. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  50. package/esm/WorkspaceLayout/dropZone.js +113 -0
  51. package/esm/WorkspaceLayout/index.d.ts +8 -0
  52. package/esm/WorkspaceLayout/index.js +4 -0
  53. package/esm/WorkspaceLayout/model.d.ts +248 -0
  54. package/esm/WorkspaceLayout/model.js +562 -0
  55. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  56. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  57. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  58. package/esm/WorkspaceLayout/spec.js +226 -0
  59. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  60. package/esm/WorkspaceLayout/splitter.js +37 -0
  61. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  62. package/esm/WorkspaceLayout/tree.js +379 -0
  63. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  64. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  65. package/esm/index.d.ts +18 -9
  66. package/esm/index.js +16 -9
  67. package/esm/menus.d.ts +55 -68
  68. package/esm/menus.js +117 -86
  69. package/esm/queryParams.d.ts +9 -0
  70. package/esm/queryParams.js +92 -0
  71. package/esm/sessionModelFactory.d.ts +18 -0
  72. package/esm/sessionModelFactory.js +1 -0
  73. package/esm/ui/App/App.d.ts +3 -14
  74. package/esm/ui/App/App.js +30 -17
  75. package/esm/ui/App/AppFab.d.ts +12 -3
  76. package/esm/ui/App/AppFab.js +15 -8
  77. package/esm/ui/App/AppReadyMarker.d.ts +43 -0
  78. package/esm/ui/App/AppReadyMarker.js +56 -0
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +23 -24
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerControls.d.ts +1 -5
  88. package/esm/ui/App/DrawerControls.js +1 -25
  89. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  90. package/esm/ui/App/DrawerHeader.js +24 -10
  91. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  92. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  93. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  94. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  95. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  96. package/esm/ui/App/DrawerWidget.js +9 -21
  97. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  98. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +144 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +9 -2
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +42 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +3 -8
  118. package/esm/ui/App/ViewsContainer.js +9 -6
  119. package/esm/ui/App/copyView.js +1 -1
  120. package/esm/ui/App/index.d.ts +2 -1
  121. package/esm/ui/App/index.js +3 -2
  122. package/esm/ui/App/lazyChunk.d.ts +13 -0
  123. package/esm/ui/App/lazyChunk.js +91 -0
  124. package/esm/ui/App/types.d.ts +12 -3
  125. package/esm/ui/App/types.js +1 -1
  126. package/esm/ui/App/useViewVisibility.d.ts +34 -0
  127. package/esm/ui/App/useViewVisibility.js +87 -0
  128. package/esm/ui/App/viewTitle.d.ts +5 -0
  129. package/esm/ui/App/viewTitle.js +9 -0
  130. package/esm/ui/index.d.ts +1 -1
  131. package/esm/ui/index.js +1 -1
  132. package/package.json +12 -17
  133. package/esm/AppFocus/index.d.ts +0 -5
  134. package/esm/AppFocus/index.js +0 -12
  135. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  136. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  137. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  138. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  139. package/esm/DockviewLayout/index.d.ts +0 -35
  140. package/esm/DockviewLayout/index.js +0 -111
  141. package/esm/ui/App/DockviewContext.d.ts +0 -22
  142. package/esm/ui/App/DockviewContext.js +0 -29
  143. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  144. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  145. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  146. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  147. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  148. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  149. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  150. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  151. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  152. package/esm/ui/App/JBrowseViewTab.js +0 -97
  153. package/esm/ui/App/ModalWidget.d.ts +0 -6
  154. package/esm/ui/App/ModalWidget.js +0 -48
  155. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  156. package/esm/ui/App/TiledViewsContainer.js +0 -366
  157. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  158. 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 {};