@jbrowse/app-core 5.0.0-beta.7 → 5.0.0-beta.9

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.
@@ -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';
@@ -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';
@@ -11,11 +11,67 @@ 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
- export declare function loadSessionSpec({ views, sessionAssemblies, sessionConnections, sessionTracks, layout, sessionName, }: {
14
+ export declare function unknownViewTypesMessage(types: string[]): string;
15
+ export declare function noLauncherMessage(types: string[]): string;
16
+ /**
17
+ * Why one view type cannot launch from a spec here, or nothing. Two causes,
18
+ * two messages: the type is unknown at all (a typo, or a plugin that was not
19
+ * loaded), or it exists but nothing taught it how to launch from a spec, which
20
+ * only the view's plugin can fix.
21
+ */
22
+ export declare function viewTypeProblem(pluginManager: PluginManager, type: string): string | undefined;
23
+ /**
24
+ * A spec view entry as its launcher takes it, and what its type does not take.
25
+ *
26
+ * The same classification a view snapshot gets, run here because a spec never
27
+ * becomes one: `LaunchView-<type>` takes these keys as arguments, so
28
+ * `withLaunchInput`'s partition never sees them and nothing names a typo.
29
+ * `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the launcher's
30
+ * downstream "No assembly provided", on the one surface written by hand with no
31
+ * compiler and no editor behind it.
32
+ *
33
+ * A view type that registers no launch keys classifies nothing: its launcher's
34
+ * vocabulary is undeclared, so every argument would read as a typo. A lazily
35
+ * registered type declares its properties on a state model that is not loaded
36
+ * yet, so it is loaded first — the launch would have loaded it anyway.
37
+ */
38
+ export declare function launchableSpecView(pluginManager: PluginManager, spec: ViewSpec): Promise<{
39
+ view: ViewSpec;
40
+ problem?: string;
41
+ }>;
42
+ /**
43
+ * Launch one spec view into the session through its `LaunchView-<type>`
44
+ * extension point, the same door `loadSessionSpec` uses for each of its views
45
+ * — so `jb.addView` and a spec agree on what a view entry means, a
46
+ * ProteinView's `connectedView` included.
47
+ *
48
+ * Strict so a launch handler that throws (missing/invalid assembly, unresolved
49
+ * track, ...) is returned as a failure instead of being swallowed by the plain
50
+ * extension-point runner, which would leave a silent empty session. A handler
51
+ * that legitimately no-ops returns normally and is unaffected.
52
+ *
53
+ * `created` is EVERY view the launch added, in creation order: a connected
54
+ * ProteinView creates its genome view and then itself. `view` is the entry's
55
+ * own, the one of the spec's type, which is where `displayName` goes — it used
56
+ * to land on the genome view a ProteinView opened first, and the structure came
57
+ * up "Untitled view". `type` is the dispatch key, not a setting: forwarding it
58
+ * would land in the view's launch blob and trip the "ignored unknown key(s):
59
+ * type" warning meant to catch typos.
60
+ */
61
+ export declare function launchSpecView(session: AbstractSessionModel | undefined, pluginManager: PluginManager, spec: ViewSpec): Promise<{
62
+ created: import("@jbrowse/core/util").AbstractViewModel[];
63
+ view: import("@jbrowse/core/util").AbstractViewModel | undefined;
64
+ failure?: {
65
+ message: string;
66
+ cause: unknown;
67
+ } | undefined;
68
+ }>;
69
+ export interface SessionSpec {
15
70
  views: ViewSpec[];
16
71
  sessionAssemblies?: Record<string, unknown>[];
17
72
  sessionConnections?: Record<string, unknown>[];
18
73
  sessionTracks?: Record<string, unknown>[];
19
74
  layout?: LayoutNode;
20
75
  sessionName?: string;
21
- }, pluginManager: PluginManager): Promise<void>;
76
+ }
77
+ export declare function loadSessionSpec(spec: SessionSpec, pluginManager: PluginManager): Promise<void>;
@@ -156,15 +156,159 @@ function flattenedTabsContainers(layout) {
156
156
  const nestedTabsChild = layout.direction === 'tabs' && children.some(c => c.children !== undefined);
157
157
  return nestedTabsChild || children.some(flattenedTabsContainers);
158
158
  }
159
- // use extension point named e.g. LaunchView-LinearGenomeView to initialize an
160
- // LGV session
161
- export async function loadSessionSpec({ views, sessionAssemblies = [], sessionConnections = [], sessionTracks = [], layout, sessionName, }, pluginManager) {
159
+ export function unknownViewTypesMessage(types) {
160
+ return `Unknown view type(s) in session spec: ${types.join(', ')}. The plugin providing the view may be missing, or the type may be misspelled.`;
161
+ }
162
+ export function noLauncherMessage(types) {
163
+ return `View type(s) ${types.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`;
164
+ }
165
+ /**
166
+ * Why one view type cannot launch from a spec here, or nothing. Two causes,
167
+ * two messages: the type is unknown at all (a typo, or a plugin that was not
168
+ * loaded), or it exists but nothing taught it how to launch from a spec, which
169
+ * only the view's plugin can fix.
170
+ */
171
+ export function viewTypeProblem(pluginManager, type) {
172
+ if (pluginManager.extensionPoints.has(`LaunchView-${type}`)) {
173
+ return undefined;
174
+ }
175
+ // the record's `has`, not getElementType, which throws on an unregistered
176
+ // name rather than returning undefined
177
+ return pluginManager.getElementTypeRecord('view').has(type)
178
+ ? noLauncherMessage([type])
179
+ : unknownViewTypesMessage([type]);
180
+ }
181
+ // v4 nested a view's settings under `init`. A spec never becomes a snapshot, so
182
+ // `withLaunchInput`'s unwrap never runs on one and this surface does it itself,
183
+ // in the same order: the flat spelling wins.
184
+ function flattenSpecView(spec) {
185
+ const { init, ...view } = spec;
186
+ if (init) {
187
+ console.warn(legacyInitMessage(view.type));
188
+ }
189
+ return init ? { ...init, ...view } : view;
190
+ }
191
+ /**
192
+ * A spec view entry as its launcher takes it, and what its type does not take.
193
+ *
194
+ * The same classification a view snapshot gets, run here because a spec never
195
+ * becomes one: `LaunchView-<type>` takes these keys as arguments, so
196
+ * `withLaunchInput`'s partition never sees them and nothing names a typo.
197
+ * `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the launcher's
198
+ * downstream "No assembly provided", on the one surface written by hand with no
199
+ * compiler and no editor behind it.
200
+ *
201
+ * A view type that registers no launch keys classifies nothing: its launcher's
202
+ * vocabulary is undeclared, so every argument would read as a typo. A lazily
203
+ * registered type declares its properties on a state model that is not loaded
204
+ * yet, so it is loaded first — the launch would have loaded it anyway.
205
+ */
206
+ export async function launchableSpecView(pluginManager, spec) {
207
+ const flat = flattenSpecView(spec);
208
+ if (!pluginManager.getElementTypeRecord('view').has(flat.type)) {
209
+ return { view: flat };
210
+ }
211
+ const viewType = pluginManager.getViewType(flat.type);
212
+ await viewType.loadStateModel();
213
+ const accepted = viewType.acceptedKeys;
214
+ if (!accepted) {
215
+ return { view: flat };
216
+ }
217
+ const { view, problem } = readAssemblyNames(flat, accepted);
218
+ const { type, ...keyed } = view;
219
+ const unknown = Object.keys(keyed).filter(key => !accepted.includes(key));
220
+ const problems = [
221
+ ...(unknown.length ? [unknownKeysMessage(type, unknown)] : []),
222
+ ...(problem ? [problem] : []),
223
+ ];
224
+ return { view, problem: problems.join('; ') || undefined };
225
+ }
226
+ // An agent reads `assemblyNames` off the live view and writes it back into a
227
+ // spec, where the key the entry takes is `assembly`. One name is that assembly;
228
+ // several are a question one view cannot answer.
229
+ function readAssemblyNames(view, accepted) {
230
+ const { assemblyNames, ...rest } = view;
231
+ // Array.isArray, not a length check: a hand-written `assemblyNames: 'volvox'`
232
+ // has a length too, and reached .join below — a throw, which this one outer
233
+ // catch turns into every remaining view and the layout being lost
234
+ if (!Array.isArray(assemblyNames) ||
235
+ !assemblyNames.length ||
236
+ !accepted.includes('assembly') ||
237
+ accepted.includes('assemblyNames')) {
238
+ return { view };
239
+ }
240
+ return assemblyNames.length === 1
241
+ ? { view: { ...rest, assembly: rest.assembly ?? assemblyNames[0] } }
242
+ : {
243
+ view: rest,
244
+ problem: `${view.type} takes one "assembly", and assemblyNames names ${assemblyNames.length} (${assemblyNames.join(', ')}) — pass assembly, one view per assembly`,
245
+ };
246
+ }
247
+ /**
248
+ * Launch one spec view into the session through its `LaunchView-<type>`
249
+ * extension point, the same door `loadSessionSpec` uses for each of its views
250
+ * — so `jb.addView` and a spec agree on what a view entry means, a
251
+ * ProteinView's `connectedView` included.
252
+ *
253
+ * Strict so a launch handler that throws (missing/invalid assembly, unresolved
254
+ * track, ...) is returned as a failure instead of being swallowed by the plain
255
+ * extension-point runner, which would leave a silent empty session. A handler
256
+ * that legitimately no-ops returns normally and is unaffected.
257
+ *
258
+ * `created` is EVERY view the launch added, in creation order: a connected
259
+ * ProteinView creates its genome view and then itself. `view` is the entry's
260
+ * own, the one of the spec's type, which is where `displayName` goes — it used
261
+ * to land on the genome view a ProteinView opened first, and the structure came
262
+ * up "Untitled view". `type` is the dispatch key, not a setting: forwarding it
263
+ * would land in the view's launch blob and trip the "ignored unknown key(s):
264
+ * type" warning meant to catch typos.
265
+ */
266
+ export async function launchSpecView(session, pluginManager, spec) {
267
+ const { type, displayName, ...view } = spec;
268
+ const before = new Set(session?.views.map(v => v.id));
269
+ let failure;
270
+ try {
271
+ await pluginManager.evaluateAsyncExtensionPointStrict(`LaunchView-${type}`, {
272
+ ...view,
273
+ session,
274
+ });
275
+ }
276
+ catch (e) {
277
+ console.error(e);
278
+ failure = { message: `Failed to launch ${type} view: ${e}`, cause: e };
279
+ }
280
+ const created = session?.views.filter(v => !before.has(v.id)) ?? [];
281
+ const named = created.find(v => v.type === type) ?? created[0];
282
+ if (named && displayName) {
283
+ named.setDisplayName(displayName);
284
+ }
285
+ return { created, view: named, ...(failure ? { failure } : {}) };
286
+ }
287
+ const SPEC_KEYS = [
288
+ 'views',
289
+ 'sessionAssemblies',
290
+ 'sessionConnections',
291
+ 'sessionTracks',
292
+ 'layout',
293
+ 'sessionName',
294
+ ];
295
+ export async function loadSessionSpec(spec, pluginManager) {
296
+ const { views, sessionAssemblies = [], sessionConnections = [], sessionTracks = [], layout, sessionName, } = spec;
162
297
  const rootModel = pluginManager.rootModel;
163
298
  try {
164
299
  rootModel.setSession?.({
165
300
  name: sessionName ?? `New session ${new Date().toLocaleString()}`,
166
301
  });
167
302
  const { session } = rootModel;
303
+ // `tracks` for sessionTracks, `assemblies` for sessionAssemblies, `name`
304
+ // for sessionName: a top-level key this spec does not define used to be
305
+ // dropped by the destructure above with nothing said, on the one door every
306
+ // agent-facing text names first. Reported like the misplaced keys below,
307
+ // rather than thrown, so the rest of the spec still loads.
308
+ const unknownKeys = Object.keys(spec).filter(key => !SPEC_KEYS.includes(key));
309
+ if (unknownKeys.length) {
310
+ session?.notifyError(`${unknownKeysMessage('Session spec', unknownKeys)} — it takes ${SPEC_KEYS.join(', ')}`);
311
+ }
168
312
  // Assemblies first: sessionTracks and the views below reference them by
169
313
  // name, so a self-contained spec (novel assemblies + their tracks, no
170
314
  // hosted config) resolves only if the assemblies exist before either runs.
@@ -217,82 +361,21 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
217
361
  }
218
362
  }
219
363
  addSessionTracks(session, sessionTracks);
220
- // a view type with no registered LaunchView-<type> extension point makes
364
+ // A view type with no registered LaunchView-<type> extension point makes
221
365
  // evaluateAsyncExtensionPoint a silent no-op, leaving an empty session with
222
- // no diagnostic. Two different causes, so two messages: the view type is
223
- // unknown here at all (a typo, or a plugin that wasn't loaded), or it exists
224
- // but nothing taught it how to launch from a spec, which the spec author
225
- // can't fix, only the view's plugin can.
366
+ // no diagnostic. Reported once per type, not per view.
367
+ const viewTypes = pluginManager.getElementTypeRecord('view');
226
368
  const notLaunchable = [
227
- ...new Set(views.flatMap(view => pluginManager.extensionPoints.has(`LaunchView-${view.type}`)
228
- ? []
229
- : [view.type])),
369
+ ...new Set(views.flatMap(view => viewTypeProblem(pluginManager, view.type) ? [view.type] : [])),
230
370
  ];
231
- // the record's `has`, not getElementType, which throws on an unregistered
232
- // name rather than returning undefined
233
- const viewTypes = pluginManager.getElementTypeRecord('view');
234
371
  const unknown = notLaunchable.filter(type => !viewTypes.has(type));
235
372
  const noLauncher = notLaunchable.filter(type => viewTypes.has(type));
236
373
  if (unknown.length) {
237
- 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.`);
374
+ session?.notifyError(unknownViewTypesMessage(unknown));
238
375
  }
239
376
  if (noLauncher.length) {
240
- session?.notifyError(`View type(s) ${noLauncher.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`);
241
- }
242
- // v4 nested a view's settings under `init`. A spec never becomes a
243
- // snapshot, so `withLaunchInput`'s unwrap never runs on one and this
244
- // surface does it itself, in the same order: the flat spelling wins.
245
- const specViews = views.map(({ init, ...view }) => {
246
- if (init) {
247
- console.warn(legacyInitMessage(view.type));
248
- }
249
- return init ? { ...init, ...view } : view;
250
- });
251
- // The same classification a view snapshot gets, run here because a spec
252
- // never becomes one: `LaunchView-<type>` takes these keys as arguments, so
253
- // `withLaunchInput`'s partition never sees them and nothing names a typo.
254
- // `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the
255
- // launcher's downstream "No assembly provided", on the one surface written
256
- // by hand with no compiler and no editor behind it.
257
- //
258
- // An ERROR rather than the snapshot path's warning: this surface's
259
- // misplaced keys already report as errors, and the launcher's own failure
260
- // lands as an error a line later — a warning under it reads as the lesser
261
- // of the two, when it is the cause.
262
- //
263
- // A view type that registers no launch keys classifies nothing: its
264
- // launcher's vocabulary is undeclared, so every argument would read as a
265
- // typo.
266
- //
267
- // A lazily registered view type declares its properties on a state model
268
- // that is not loaded yet, so it is loaded first — the launch below would
269
- // have loaded it anyway.
270
- await Promise.all(specViews
271
- .filter(({ type }) => viewTypes.has(type))
272
- .map(({ type }) => pluginManager.getViewType(type).loadStateModel()));
273
- for (const { type, ...view } of specViews) {
274
- const accepted = viewTypes.has(type)
275
- ? pluginManager.getViewType(type).acceptedKeys
276
- : undefined;
277
- const unknown = accepted
278
- ? Object.keys(view).filter(key => !accepted.includes(key))
279
- : [];
280
- if (unknown.length) {
281
- session?.notifyError(unknownKeysMessage(type, unknown));
282
- }
377
+ session?.notifyError(noLauncherMessage(noLauncher));
283
378
  }
284
- // Launch sequentially and record the id each spec view created, so the
285
- // layout below can map its indices to real views. Reading session.views
286
- // positionally afterwards only works while every handler happens to addView
287
- // synchronously and in order; capturing the delta per launch instead is
288
- // correct even if a handler awaits or adds an auxiliary view, and lets a
289
- // later spec view (e.g. a connected MsaView) reference an earlier one that
290
- // now already exists. `type` is the dispatch key, not a setting: forwarding
291
- // it would land in the view's launch blob and trip the spurious
292
- // "ignored unknown key(s): type" warning meant to catch typos.
293
- // `displayName` is applied here rather than forwarded because it is a base
294
- // view prop every view type has, so one path covers all of them (including
295
- // plugin-provided types whose launcher never heard of it).
296
379
  // Let any connection this spec registered finish before the views that
297
380
  // reference what it supplies are launched (see whenConnectionsSettle). Gated
298
381
  // on there being views: with none, nothing is waiting on the connection, and
@@ -306,31 +389,28 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
306
389
  !(await whenConnectionsSettle(session))) {
307
390
  return;
308
391
  }
392
+ // Launch sequentially and record the ids each spec view created, so the
393
+ // layout below can map its indices to real views. Reading session.views
394
+ // positionally afterwards only works while every handler happens to addView
395
+ // synchronously and in order; capturing the delta per launch instead is
396
+ // correct even if a handler awaits or adds an auxiliary view, and lets a
397
+ // later spec view (e.g. a connected MsaView) reference an earlier one that
398
+ // now already exists.
399
+ //
400
+ // An unknown key is an ERROR rather than the snapshot path's warning: this
401
+ // surface's misplaced keys already report as errors, and the launcher's own
402
+ // failure lands as an error a line later — a warning under it reads as the
403
+ // lesser of the two, when it is the cause. Per view, so one bad view
404
+ // doesn't cost the rest.
309
405
  const createdViewIds = [];
310
- for (const { type, displayName, ...view } of specViews) {
311
- const before = new Set(session?.views.map(v => v.id));
312
- // Strict so a launch handler that throws (missing/invalid assembly,
313
- // unresolved track, ...) surfaces as a snackbar instead of being swallowed
314
- // by the plain extension-point runner, which would leave a silent empty
315
- // session. A handler that legitimately no-ops returns normally and is
316
- // unaffected. Per-view try/catch so one bad view doesn't abort the rest.
317
- try {
318
- await pluginManager.evaluateAsyncExtensionPointStrict(`LaunchView-${type}`, {
319
- ...view,
320
- session,
321
- });
322
- }
323
- catch (e) {
324
- console.error(e);
325
- session?.notifyError(`Failed to launch ${type} view: ${e}`, e);
406
+ for (const spec of views) {
407
+ const { view, problem } = await launchableSpecView(pluginManager, spec);
408
+ if (problem) {
409
+ session?.notifyError(problem);
326
410
  }
327
- const created = session?.views.filter(v => !before.has(v.id)) ?? [];
328
- // The entry's own view, when its launcher created others beside it: a
329
- // connected ProteinView's name went to the genome view it opened first,
330
- // and the structure came up "Untitled view".
331
- const named = created.find(v => v.type === type) ?? created[0];
332
- if (named && displayName) {
333
- named.setDisplayName(displayName);
411
+ const { created, failure } = await launchSpecView(session, pluginManager, view);
412
+ if (failure) {
413
+ session?.notifyError(failure.message, failure.cause);
334
414
  }
335
415
  createdViewIds.push(created.map(v => v.id));
336
416
  }
@@ -6,6 +6,12 @@ export interface ViewSpec {
6
6
  displayName?: string;
7
7
  tracks?: TrackInit[];
8
8
  assembly?: string;
9
+ /**
10
+ * The live view's spelling of the key above, accepted because that is where
11
+ * a spec gets written from: one name is the `assembly`, and several are
12
+ * refused naming it.
13
+ */
14
+ assemblyNames?: string[];
9
15
  loc?: string;
10
16
  /**
11
17
  * @deprecated v4's nesting. Write every setting directly on the view object;
@@ -243,14 +243,14 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
243
243
  tileViews(mode: TileMode, allViewIds: string[]): void;
244
244
  /**
245
245
  * #action
246
- * Arrange the session's views into panels: `applyLayoutSpec` plus the
247
- * two things it does not do on its own, each of which fails silently
248
- * without it — turning workspaces mode on for this session (a layout
249
- * renders nowhere else) and ordering `session.views` to the spec's
250
- * top-to-bottom statement, which is where a tab reads its order from.
251
- * Leaves take view ids or indexes into `session.views`; a layout that
252
- * seats no view throws rather than leaving a blank tab. What a session
253
- * spec's `layout` and an agent's live re-layout both call.
246
+ * Arrange the session's views into panels. Calls `applyLayoutSpec`,
247
+ * turns workspaces mode on for this session (a layout renders nowhere
248
+ * else), and orders `session.views` to the spec's top-to-bottom
249
+ * order, which tabs read their order from. Without the last two steps
250
+ * the layout applies but does not display, or the tabs appear in the
251
+ * wrong order. Leaves take view ids or indexes into `session.views`; a
252
+ * layout that seats no view throws rather than leaving a blank tab. A
253
+ * session spec's `layout` and an agent's live re-layout both call it.
254
254
  */
255
255
  layoutViews(spec: LayoutSpecNode): string[];
256
256
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
@@ -560,14 +560,14 @@ export function WorkspaceLayoutMixin() {
560
560
  },
561
561
  /**
562
562
  * #action
563
- * Arrange the session's views into panels: `applyLayoutSpec` plus the
564
- * two things it does not do on its own, each of which fails silently
565
- * without it — turning workspaces mode on for this session (a layout
566
- * renders nowhere else) and ordering `session.views` to the spec's
567
- * top-to-bottom statement, which is where a tab reads its order from.
568
- * Leaves take view ids or indexes into `session.views`; a layout that
569
- * seats no view throws rather than leaving a blank tab. What a session
570
- * spec's `layout` and an agent's live re-layout both call.
563
+ * Arrange the session's views into panels. Calls `applyLayoutSpec`,
564
+ * turns workspaces mode on for this session (a layout renders nowhere
565
+ * else), and orders `session.views` to the spec's top-to-bottom
566
+ * order, which tabs read their order from. Without the last two steps
567
+ * the layout applies but does not display, or the tabs appear in the
568
+ * wrong order. Leaves take view ids or indexes into `session.views`; a
569
+ * layout that seats no view throws rather than leaving a blank tab. A
570
+ * session spec's `layout` and an agent's live re-layout both call it.
571
571
  */
572
572
  layoutViews(spec) {
573
573
  const host = layoutHost(self);
package/esm/ui/App/App.js CHANGED
@@ -2,11 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Suspense, lazy } from 'react';
3
3
  import Snackbar from '@jbrowse/core/ui/Snackbar';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { ModalWidget, drawerGridTemplateColumns } from '@jbrowse/product-core';
5
+ import { AppReadyMarker, ModalWidget, drawerGridTemplateColumns, } from '@jbrowse/product-core';
6
6
  import { AppBar } from '@mui/material';
7
7
  import { observer } from 'mobx-react';
8
8
  import AppFab from './AppFab.js';
9
- import AppReadyMarker from './AppReadyMarker.js';
10
9
  import AppToolbar from './AppToolbar.js';
11
10
  import DialogQueue from './DialogQueue.js';
12
11
  import ViewsContainer from './ViewsContainer.js';
@@ -10,6 +10,12 @@ import ViewContainer from './ViewContainer.js';
10
10
  // the amount that makes that gesture reach, and any more is room to scroll the
11
11
  // content off its own surface.
12
12
  //
13
+ // It is scrollable room rather than content, so anything measuring how tall the
14
+ // session is discounts it by its `view-stack-overscroll` testid — `overflow` in
15
+ // jbApi does, and a filmed take paid for that: counted as content, it told
16
+ // `jb.fitToWindow` the session was 300px taller than it was, and the dotplot
17
+ // absorbing the cut came out a third of the height that fit.
18
+ //
13
19
  // Capped, because half a port stops being a courtesy on a tall screen. 300px is
14
20
  // the cap the classic stack has always had and it stays the answer there — a
15
21
  // full window is comfortably past twice it. What the cap was hiding is the
@@ -37,6 +43,6 @@ const ViewStack = observer(function ViewStack({ views, session, className, }) {
37
43
  // restore / initial load), so they must not steal scroll when they mount. A
38
44
  // view whose id isn't in this set was added later and auto-scrolls into view.
39
45
  const [initialViewIds] = useState(() => new Set(views.map(v => v.id)));
40
- return (_jsxs(_Fragment, { children: [_jsx("div", { ref: viewsRef, className: className, children: views.map(view => (_jsx(ViewContainer, { view: view, session: session, scrollOnMount: !initialViewIds.has(view.id) }, view.id))) }), scrollable ? _jsx("div", { className: classes.spacer }) : null] }));
46
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: viewsRef, className: className, children: views.map(view => (_jsx(ViewContainer, { view: view, session: session, scrollOnMount: !initialViewIds.has(view.id) }, view.id))) }), scrollable ? (_jsx("div", { className: classes.spacer, "data-testid": "view-stack-overscroll" })) : null] }));
41
47
  });
42
48
  export default ViewStack;
@@ -1,3 +1,3 @@
1
1
  export { App } from './App.tsx';
2
- export { default as AppReadyMarker } from './AppReadyMarker.tsx';
2
+ export { AppReadyMarker } from '@jbrowse/product-core';
3
3
  export { default as DialogQueue } from './DialogQueue.tsx';
@@ -1,3 +1,3 @@
1
1
  export { App } from './App.js';
2
- export { default as AppReadyMarker } from './AppReadyMarker.js';
2
+ export { AppReadyMarker } from '@jbrowse/product-core';
3
3
  export { default as DialogQueue } from './DialogQueue.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/app-core",
3
- "version": "5.0.0-beta.7",
3
+ "version": "5.0.0-beta.9",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 code shared between the 'full featured' apps e.g. jbrowse-web and jbrowse-desktop",
6
6
  "keywords": [
@@ -24,13 +24,13 @@
24
24
  "esm"
25
25
  ],
26
26
  "dependencies": {
27
- "@jbrowse/mobx-state-tree": "^6.5.1",
27
+ "@jbrowse/mobx-state-tree": "^6.5.2",
28
28
  "@mui/icons-material": "^9.4.0",
29
29
  "@mui/material": "^9.4.0",
30
30
  "mobx": "^7.0.3",
31
31
  "mobx-react": "^10.0.2",
32
- "@jbrowse/product-core": "^5.0.0-beta.7",
33
- "@jbrowse/core": "^5.0.0-beta.7"
32
+ "@jbrowse/core": "^5.0.0-beta.9",
33
+ "@jbrowse/product-core": "^5.0.0-beta.9"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=19.0.0",
@@ -1,43 +0,0 @@
1
- import type { AppSession } from './types.ts';
2
- /**
3
- * One element that says whether the whole app has finished, and what is open.
4
- *
5
- * `[data-app-phase="ready"]` is the entire readiness contract for anything
6
- * driving JBrowse from outside — a screenshot tool, a test, an agent. Wait for
7
- * that selector and you are done; there is no chain to assemble and no
8
- * per-display census to take.
9
- *
10
- * Why it exists when `data-view-phase` and `data-display-phase` already do:
11
- * those are per-element and NEGATIVE. Asking them "is everything finished"
12
- * means asserting that no element carries the loading value, which is equally
13
- * true of an app that has not started — and the gap is real, measured at about
14
- * a second on a two-track session. A positive attribute cannot be satisfied
15
- * early, because something has to render it.
16
- *
17
- * The census attributes beside it — `data-app-views`, `data-app-assemblies`,
18
- * `data-app-tracks` (the latter two JSON arrays) — publish WHAT is open the
19
- * same way the phase publishes whether it is done, so an outside reader asking
20
- * "is the track I requested actually open" reads one element instead of
21
- * walking `window.JBrowseSession` with its own copy of the view nesting. The
22
- * walk itself is `openViews`/`openTracks` over each view's declared
23
- * `ownViews`/`ownTracks`, so this component knows nothing about which property
24
- * a container view keeps its children on either.
25
- *
26
- * Its own component, and `hidden`, for two reasons: an observer here subscribes
27
- * to every view's phase without re-rendering the app shell around it, and a
28
- * marker that is never drawn cannot be mistaken for UI or shift a layout. It is
29
- * still found by `querySelector`, which is what reads it.
30
- *
31
- * **What it is silent about**: a display in a TERMINAL state. `error` is a
32
- * finished state, so this reads `ready` over a display whose fetch failed —
33
- * correctly, since nothing is still working. A capture wants more than that: it
34
- * wants a picture, and an error banner is not one. `waitForDisplaysDone` after
35
- * this is what draws that line, keying on the `data-display-drawn` the two
36
- * comparative canvases publish from their stricter `settled` gate (see
37
- * `comparativeReadiness` in `@jbrowse/synteny-core`, which holds both answers
38
- * and says why an error separates them). Both harnesses do it in that order.
39
- */
40
- declare const AppReadyMarker: ({ session, }: {
41
- session: AppSession;
42
- }) => import("react").JSX.Element;
43
- export default AppReadyMarker;