@jbrowse/product-core 5.0.0-beta.8 → 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.
Files changed (64) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +1 -4
  2. package/esm/RootModel/HierarchicalConfig.d.ts +21 -47
  3. package/esm/RootModel/PreferencesConfig.d.ts +18 -49
  4. package/esm/RootModel/PreferencesConfig.js +2 -3
  5. package/esm/RootModel/createConfigModel.d.ts +141 -124
  6. package/esm/RootModel/createConfigModel.js +26 -14
  7. package/esm/RootModel/menuItems.js +4 -6
  8. package/esm/Session/BaseSession.d.ts +30 -13
  9. package/esm/Session/BaseSession.js +50 -16
  10. package/esm/Session/Connections.d.ts +4 -3
  11. package/esm/Session/Connections.js +4 -3
  12. package/esm/Session/DrawerWidgets.d.ts +8 -1
  13. package/esm/Session/DrawerWidgets.js +19 -7
  14. package/esm/Session/MultipleViews.d.ts +13 -14
  15. package/esm/Session/Preferences.d.ts +8 -13
  16. package/esm/Session/SessionTracks.d.ts +32 -38
  17. package/esm/Session/SessionTracks.js +52 -115
  18. package/esm/Session/Themes.d.ts +9 -10
  19. package/esm/Session/Themes.js +9 -10
  20. package/esm/Session/TrackMenu.d.ts +1 -26
  21. package/esm/Session/TrackMenu.js +2 -33
  22. package/esm/Session/TrackMenuSessionMixin.js +2 -1
  23. package/esm/Session/Tracks.d.ts +8 -13
  24. package/esm/Session/copyTrackSnapshot.d.ts +21 -0
  25. package/esm/Session/copyTrackSnapshot.js +20 -0
  26. package/esm/Session/hydratedForms.d.ts +16 -0
  27. package/esm/Session/hydratedForms.js +13 -0
  28. package/esm/Session/index.d.ts +5 -1
  29. package/esm/Session/index.js +4 -1
  30. package/esm/Session/pluginExtraTrackItems.d.ts +11 -0
  31. package/esm/Session/pluginExtraTrackItems.js +13 -0
  32. package/esm/controllerTracks.d.ts +13 -8
  33. package/esm/controllerTracks.js +18 -9
  34. package/esm/index.d.ts +6 -6
  35. package/esm/index.js +3 -3
  36. package/esm/loadPlugins.d.ts +7 -2
  37. package/esm/loadPlugins.js +6 -2
  38. package/esm/resolveAssemblies.d.ts +34 -11
  39. package/esm/resolveAssemblies.js +29 -11
  40. package/esm/rpcWorker.d.ts +12 -5
  41. package/esm/rpcWorker.js +1 -1
  42. package/esm/sessionMigrations/index.d.ts +5 -5
  43. package/esm/sessionMigrations/index.js +82 -19
  44. package/esm/sessionUrl.js +1 -1
  45. package/esm/sessionUtils.d.ts +6 -1
  46. package/esm/sessionUtils.js +42 -35
  47. package/esm/ui/AboutDialog.d.ts +10 -2
  48. package/esm/ui/AboutDialog.js +11 -2
  49. package/esm/ui/AboutDialogContents.js +3 -21
  50. package/esm/ui/AppReadyMarker.d.ts +45 -0
  51. package/esm/ui/AppReadyMarker.js +89 -0
  52. package/esm/ui/AssemblyInfoPanel.js +4 -10
  53. package/esm/ui/DescriptionPanel.js +2 -1
  54. package/esm/ui/FileInfoPanel.js +5 -10
  55. package/esm/ui/RefNameInfoDialog.js +10 -23
  56. package/esm/ui/aboutTestUtils.d.ts +140 -0
  57. package/esm/ui/aboutTestUtils.js +25 -0
  58. package/esm/ui/descriptionHtml.d.ts +5 -6
  59. package/esm/ui/descriptionHtml.js +5 -7
  60. package/esm/ui/index.d.ts +2 -0
  61. package/esm/ui/index.js +1 -0
  62. package/esm/ui/util.d.ts +9 -27
  63. package/esm/ui/util.js +12 -31
  64. package/package.json +4 -4
@@ -235,17 +235,14 @@ function withoutBaseAssemblies(sessionAssemblies, baseAssemblyNames) {
235
235
  // `withoutBaseAssemblies` drops to keep the recipient's safeReferences
236
236
  // unambiguous.
237
237
  //
238
- // Edited-ness is `flattenTrackConfigDelta`, the same gate `splitTracksAgainstBase`
239
- // uses, so an export can't call an assembly edited on a diff too thin to be worth
238
+ // Edited-ness is `editedDelta`, the same gate `splitTracksAgainstBase` uses, so
239
+ // an export can't call an assembly edited on a diff too thin to be worth
240
240
  // shipping for a track.
241
- function assembliesRevertedToBase(assemblies, sessionAssemblies, baseAssemblies) {
241
+ function assembliesRevertedToBase(assemblies, sessionAssemblies, baseAssemblies, form) {
242
242
  const baseByName = new Map(baseAssemblies.map(a => [a.name, a]));
243
243
  const edited = assemblies.flatMap(assembly => {
244
244
  const base = baseByName.get(assembly.name);
245
- return base &&
246
- flattenTrackConfigDelta(base, diffTrackConfig(base, assembly)).length > 0
247
- ? [assembly.name]
248
- : [];
245
+ return base && editedDelta(base, assembly, form) ? [assembly.name] : [];
249
246
  });
250
247
  const shadowed = sessionAssemblies.flatMap(a => {
251
248
  const name = readAssemblyName(a);
@@ -275,17 +272,19 @@ function droppableTrackIds(tracks, sessionTracks) {
275
272
  // (`addTrackTextSearchConf`), so indexing a remote BAM on desktop is enough to
276
273
  // make an otherwise perfectly portable track non-portable and get it dropped
277
274
  // whole. The data file is the track; the index is a search box over it. Shedding
278
- // the index costs the recipient that box and nothing else, and under a hosted
279
- // base the omission also lets the base's own (remote) index resolve, since a
280
- // delta records adds and changes but never a deletion.
281
- function withoutLocalTextSearch(track) {
275
+ // the index costs the recipient that box and nothing else.
276
+ //
277
+ // A hub track takes the hub's own index in its place, so its delta records no
278
+ // change there and the recipient searches with the hub's index. Leaving the
279
+ // member out would be a reset of it.
280
+ function withoutLocalTextSearch(track, hubIndex) {
282
281
  if (!isRecord(track) ||
283
282
  !isRecord(track.textSearching) ||
284
283
  analyzeWebPortability(track.textSearching).portable) {
285
284
  return track;
286
285
  }
287
286
  const { textSearching, ...rest } = track;
288
- return rest;
287
+ return (hubIndex === undefined ? rest : { ...rest, textSearching: hubIndex });
289
288
  }
290
289
  // Display names of the tracks `withoutLocalTextSearch` stripped, for the report.
291
290
  function strippedTextIndexNames(before, after) {
@@ -329,23 +328,26 @@ function nonPortableUserTracks(report, droppable) {
329
328
  // but the delta channel it travels in means "user override" on the far side, and
330
329
  // a delta masks later admin changes to that track. Telling the two apart needs
331
330
  // the base as it was at session creation, which nothing persists.
332
- function splitTracksAgainstBase(tracks, baseTracks) {
331
+ function splitTracksAgainstBase(tracks, baseTracks, form) {
333
332
  const baseById = new Map(baseTracks.map(t => [t.trackId, t]));
334
333
  const addedTracks = tracks.filter(t => !baseById.has(t.trackId));
335
334
  const editDeltas = Object.fromEntries(tracks.flatMap((track) => {
336
335
  const base = baseById.get(track.trackId);
337
- if (!base) {
338
- return [];
339
- }
340
- const delta = diffTrackConfig(base, track);
341
- // gate on real slot changes, not the identity keys / injected display
342
- // stubs a raw diff carries (matches the web session's "is edited" test)
343
- return flattenTrackConfigDelta(base, delta).length > 0
344
- ? [[track.trackId, delta]]
345
- : [];
336
+ const delta = base && editedDelta(base, track, form);
337
+ return delta ? [[track.trackId, delta]] : [];
346
338
  }));
347
339
  return { addedTracks, editDeltas };
348
340
  }
341
+ // The delta from `base` to `edited` with both in their hydrated form, or
342
+ // undefined when no setting changed — the web session's own "is edited" test,
343
+ // which a bare key count fails on the identity keys and display stubs
344
+ function editedDelta(base, edited, form) {
345
+ const hydratedBase = form(base);
346
+ const delta = diffTrackConfig(hydratedBase, form(edited));
347
+ return flattenTrackConfigDelta(hydratedBase, delta).length > 0
348
+ ? delta
349
+ : undefined;
350
+ }
349
351
  // Overlays edited-track deltas onto the session's trackConfigDeltas, preserving
350
352
  // any the base session already carried. A no-op (returns the session unchanged)
351
353
  // when there are no edits, so the exported snapshot stays minimal.
@@ -443,18 +445,27 @@ function withCarriedConfigs(session, plugins, connections) {
443
445
  // Decides how to hand a desktop session to jbrowse-web. When the session was
444
446
  // launched from a hosted hub config (sourceConfigUrl) that still covers all of
445
447
  // its assemblies, that config is reused as the base and only user-added/edited
446
- // tracks ride along; otherwise the session is made self-contained. `baseConfig`
447
- // is the fetched hub config (already rebased with addRelativeUris by the caller),
448
+ // tracks ride along; otherwise the session is made self-contained. `base` holds
449
+ // the fetched hub config (already rebased with addRelativeUris by the caller),
448
450
  // used to tell hub tracks from user-added ones and to diff edited hub tracks.
449
- export function planWebExport(snapshot, baseConfig) {
451
+ export function planWebExport(snapshot, base) {
450
452
  const sourceConfigUrl = snapshot.configuration?.sourceConfigUrl;
451
453
  const assemblies = snapshot.assemblies ?? [];
452
454
  const defaultSession = snapshot.defaultSession ?? {};
453
455
  const priorSessionAssemblies = asArray(defaultSession.sessionAssemblies);
454
456
  const inputTracks = snapshot.tracks ?? [];
455
457
  const inputSessionTracks = asArray(defaultSession.sessionTracks);
456
- const allTracks = inputTracks.map(withoutLocalTextSearch);
457
- const allSessionTracks = inputSessionTracks.map(withoutLocalTextSearch);
458
+ const baseConfig = base?.config;
459
+ const baseAssemblyNames = new Set((baseConfig?.assemblies ?? []).map(a => a.name));
460
+ const coveredByBase = !!sourceConfigUrl &&
461
+ !!base &&
462
+ assemblies.every(a => baseAssemblyNames.has(a.name));
463
+ const hubIndexes = new Map((coveredByBase ? (base.config.tracks ?? []) : []).map(t => [
464
+ t.trackId,
465
+ t.textSearching,
466
+ ]));
467
+ const allTracks = inputTracks.map(t => withoutLocalTextSearch(t, hubIndexes.get(t.trackId)));
468
+ const allSessionTracks = inputSessionTracks.map(t => withoutLocalTextSearch(t));
458
469
  // over the stripped lists, so a track whose only local file was its own search
459
470
  // index is no longer read as one that has to be dropped
460
471
  const report = analyzeWebPortability({
@@ -469,11 +480,7 @@ export function planWebExport(snapshot, baseConfig) {
469
480
  };
470
481
  const tracks = allTracks.filter(keep);
471
482
  const priorSessionTracks = allSessionTracks.filter(keep);
472
- const baseAssemblyNames = new Set((baseConfig?.assemblies ?? []).map(a => a.name));
473
- const coveredByBase = !!sourceConfigUrl &&
474
- !!baseConfig &&
475
- assemblies.every(a => baseAssemblyNames.has(a.name));
476
- // `baseConfig` is undefined only because the caller could not fetch
483
+ // `base` is undefined only because the caller could not fetch
477
484
  // `sourceConfigUrl` — it is the same fetch that produces it — so the two ways
478
485
  // a hub drops out of the plan are distinguishable here without a flag.
479
486
  const selfContainedReason = coveredByBase
@@ -488,7 +495,7 @@ export function planWebExport(snapshot, baseConfig) {
488
495
  // defined exactly when the hostedConfigBase strategy applies, so it doubles as
489
496
  // the strategy flag for the shared tail below
490
497
  const hosted = coveredByBase
491
- ? splitTracksAgainstBase(tracks, baseConfig.tracks ?? [])
498
+ ? splitTracksAgainstBase(tracks, base.config.tracks ?? [], base.forms.track)
492
499
  : undefined;
493
500
  const session = withCarriedConfigs(hosted
494
501
  ? withDeltas({
@@ -525,8 +532,8 @@ export function planWebExport(snapshot, baseConfig) {
525
532
  ...strippedTextIndexNames(inputSessionTracks, allSessionTracks),
526
533
  ]),
527
534
  ],
528
- revertedAssemblies: hosted
529
- ? assembliesRevertedToBase(assemblies, priorSessionAssemblies, baseConfig?.assemblies ?? [])
535
+ revertedAssemblies: coveredByBase
536
+ ? assembliesRevertedToBase(assemblies, priorSessionAssemblies, base.config.assemblies ?? [], base.forms.assembly)
530
537
  : [],
531
538
  unavailableAccounts: unavailableAccountIds(session, hosted ? (baseConfig?.internetAccounts ?? []) : []),
532
539
  };
@@ -1,4 +1,12 @@
1
- import type { AboutPanelProps } from './util.ts';
2
- export default function AboutDialog({ config, session, handleClose, }: AboutPanelProps & {
1
+ import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
+ import type { AbstractSessionModel } from '@jbrowse/core/util';
3
+ export default function AboutDialog({ config, session, handleClose, }: {
4
+ session: AbstractSessionModel;
5
+ /**
6
+ * the in-view track label passes `track.configuration`, a live node; the
7
+ * hierarchical selector passes a `session.tracks` entry, a plain object
8
+ * until something references the track
9
+ */
10
+ config: AnyConfigurationModel | Record<string, unknown>;
3
11
  handleClose: () => void;
4
12
  }): import("react").JSX.Element;
@@ -1,11 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
3
+ import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
4
+ import { hydrateTrackConfig } from '@jbrowse/core/configuration';
2
5
  import { PluggableComponent } from '@jbrowse/core/ui';
3
6
  import Dialog from '@jbrowse/core/ui/Dialog';
4
7
  import { getEnv } from '@jbrowse/core/util';
5
8
  import { getTrackName } from '@jbrowse/core/util/tracks';
9
+ import { isStateTreeNode } from '@jbrowse/mobx-state-tree';
6
10
  import AboutContents from './AboutDialogContents.js';
7
11
  export default function AboutDialog({ config, session, handleClose, }) {
8
- const trackName = getTrackName(config, session);
9
12
  const { pluginManager } = getEnv(session);
10
- return (_jsx(Dialog, { open: true, onClose: handleClose, title: trackName, maxWidth: "xl", children: _jsx(PluggableComponent, { pluginManager: pluginManager, name: "Core-replaceAbout", component: AboutContents, props: { config, session } }) }));
13
+ // hydrated once here, so every panel and every plugin on the two About points
14
+ // reads one live node: "Copy config" gives the same JSON whichever menu
15
+ // opened the dialog, and a jexl `formatAbout` slot has an env to evaluate in
16
+ const live = isStateTreeNode(config)
17
+ ? config
18
+ : hydrateTrackConfig(pluginManager, config);
19
+ return (_jsx(Dialog, { open: true, onClose: handleClose, title: getTrackName(config, session), maxWidth: "xl", children: live ? (_jsx(PluggableComponent, { pluginManager: pluginManager, name: "Core-replaceAbout", component: AboutContents, props: { config: live, session } })) : (_jsx(BaseCard, { title: "Configuration", children: _jsx(Attributes, { attributes: config }) })) }));
11
20
  }
@@ -2,11 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
4
4
  import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
5
- import { hydrateTrackConfig } from '@jbrowse/core/configuration';
5
+ import { readConfObject } from '@jbrowse/core/configuration';
6
6
  import PluggableComponents from '@jbrowse/core/ui/PluggableComponents';
7
7
  import { getEnv } from '@jbrowse/core/util';
8
8
  import { makeStyles } from '@jbrowse/core/util/tss-react';
9
- import { getSnapshot, isStateTreeNode } from '@jbrowse/mobx-state-tree';
10
9
  import { observer } from 'mobx-react';
11
10
  import AssemblyInfoPanel from './AssemblyInfoPanel.js';
12
11
  import DescriptionPanel from './DescriptionPanel.js';
@@ -24,25 +23,8 @@ const AboutDialogContents = observer(function AboutDialogContents({ config, sess
24
23
  const { classes } = useStyles();
25
24
  const [showRefNames, setShowRefNames] = useState(false);
26
25
  const { pluginManager } = getEnv(session);
27
- // The two menus that open this dialog hand over different things: the in-view
28
- // track label passes `track.configuration`, a live node, while the
29
- // hierarchical selector passes a `session.tracks` entry, which is a
30
- // `types.frozen` plain object until something references the track. Hydrating
31
- // converges them, so "Copy config" gives the same JSON whichever menu you came
32
- // from; it returns undefined for a config no plugin can build, and that falls
33
- // back to copying it as authored.
34
- const live = isStateTreeNode(config)
35
- ? config
36
- : hydrateTrackConfig(pluginManager, config);
37
- const conf = live
38
- ? getSnapshot(live)
39
- : config;
40
- const { config: shown, hideUris } = getAboutDialogConfig({
41
- config,
42
- session,
43
- pluginManager,
44
- });
45
- return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: conf, hideUris: hideUris, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: shown.metadata, hideUris: hideUris }) })) : null, _jsx(DescriptionPanel, { config: config, session: session }), _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
26
+ const { config: shown, hideUris } = getAboutDialogConfig({ config, session });
27
+ return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: readConfObject(config), hideUris: hideUris, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: shown.metadata, hideUris: hideUris }) })) : null, _jsx(DescriptionPanel, { config: config, session: session }), _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
46
28
  /** #extensionPoint Core-extraAboutPanel | sync | Add extra panels to a track's About dialog */
47
29
  name: "Core-extraAboutPanel", props: { session, config } }), hideUris ? null : _jsx(FileInfoPanel, { config: config, session: session }), showRefNames ? (_jsx(RefNameInfoDialog, { session: session, config: config, onClose: () => {
48
30
  setShowRefNames(false);
@@ -0,0 +1,45 @@
1
+ import type { AbstractSessionModel } from '@jbrowse/core/util/types';
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` and
32
+ * `canceled` are finished states, so this reads `ready` over a display whose
33
+ * fetch failed or that the user stopped — correctly, since nothing is still
34
+ * working, and a user's cancel is durable until Retry. A capture wants more than
35
+ * that: it wants a picture, and neither banner is one. The capture census after
36
+ * this is what draws that line: `waitForDisplaysDone` keys on the
37
+ * `data-display-drawn` the two comparative canvases publish from their stricter
38
+ * `settled` gate (see `comparativeReadiness` in `@jbrowse/synteny-core`, which
39
+ * holds both answers and says why an error separates them), and the census
40
+ * names every `data-display-phase="canceled"`.
41
+ */
42
+ declare const AppReadyMarker: ({ session, }: {
43
+ session: AbstractSessionModel;
44
+ }) => import("react").JSX.Element;
45
+ export default AppReadyMarker;
@@ -0,0 +1,89 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { openTracks, openViews } from '@jbrowse/core/util/openViews';
3
+ import { observer } from 'mobx-react';
4
+ function trackLoading(track) {
5
+ return track.displays.some(d => d.displayPhase === 'loading');
6
+ }
7
+ /**
8
+ * A view whose init failed (its assembly was never found) stays uninitialized
9
+ * for good and paints its error, so like a display's terminal phase it is
10
+ * finished, not pending: without this the whole app read `loading` for as long
11
+ * as that view was open, and every settle answered false with no reason.
12
+ *
13
+ * `pendingLaunch` is the third term and the one that was missing. `initialized`
14
+ * goes true mid-launch — LGV's flips the moment displayedRegions land, while
15
+ * the same apply pass still has the spec's tracks to attach — and `showLoading`
16
+ * goes false at the same moment, deliberately, so the reader sees the
17
+ * navigated view rather than a spinner while its tracks arrive. That left a
18
+ * window, exactly as wide as attaching the tracks, in which the marker said
19
+ * `ready` over a session missing everything it had been asked for.
20
+ * `whenViewSettled` states the same rule for the callers that await a single
21
+ * view, and says why in more detail. Not shared as one predicate: that one
22
+ * requires `initialized`, while absent here means not loading, and an adapter
23
+ * between the two null rules would read as agreement it does not have.
24
+ *
25
+ * `showImportForm` excuses the `initialized` term, and only that one. The
26
+ * comparative views spell `initialized` as "every row exists and is ready" —
27
+ * LinearSyntenyView's wants `views.length > 0`, dotplot's wants regions on both
28
+ * axes — so a view clicked into existence through `Add -> Linear synteny view`
29
+ * reads uninitialized for as long as its form is up, which is until a person
30
+ * fills it in. The app sat at `loading` behind every import-form figure. The
31
+ * other two terms keep their meaning: `showLoading` is the spinner the form
32
+ * yields to, and `showImportForm` is already false while a launch is pending.
33
+ */
34
+ function viewLoading(view) {
35
+ const flags = view;
36
+ return (flags.error === undefined &&
37
+ (flags.showLoading === true ||
38
+ flags.pendingLaunch !== undefined ||
39
+ (flags.initialized === false && flags.showImportForm !== true)));
40
+ }
41
+ /**
42
+ * One element that says whether the whole app has finished, and what is open.
43
+ *
44
+ * `[data-app-phase="ready"]` is the entire readiness contract for anything
45
+ * driving JBrowse from outside — a screenshot tool, a test, an agent. Wait for
46
+ * that selector and you are done; there is no chain to assemble and no
47
+ * per-display census to take.
48
+ *
49
+ * Why it exists when `data-view-phase` and `data-display-phase` already do:
50
+ * those are per-element and NEGATIVE. Asking them "is everything finished"
51
+ * means asserting that no element carries the loading value, which is equally
52
+ * true of an app that has not started — and the gap is real, measured at about
53
+ * a second on a two-track session. A positive attribute cannot be satisfied
54
+ * early, because something has to render it.
55
+ *
56
+ * The census attributes beside it — `data-app-views`, `data-app-assemblies`,
57
+ * `data-app-tracks` (the latter two JSON arrays) — publish WHAT is open the
58
+ * same way the phase publishes whether it is done, so an outside reader asking
59
+ * "is the track I requested actually open" reads one element instead of
60
+ * walking `window.JBrowseSession` with its own copy of the view nesting. The
61
+ * walk itself is `openViews`/`openTracks` over each view's declared
62
+ * `ownViews`/`ownTracks`, so this component knows nothing about which property
63
+ * a container view keeps its children on either.
64
+ *
65
+ * Its own component, and `hidden`, for two reasons: an observer here subscribes
66
+ * to every view's phase without re-rendering the app shell around it, and a
67
+ * marker that is never drawn cannot be mistaken for UI or shift a layout. It is
68
+ * still found by `querySelector`, which is what reads it.
69
+ *
70
+ * **What it is silent about**: a display in a TERMINAL state. `error` and
71
+ * `canceled` are finished states, so this reads `ready` over a display whose
72
+ * fetch failed or that the user stopped — correctly, since nothing is still
73
+ * working, and a user's cancel is durable until Retry. A capture wants more than
74
+ * that: it wants a picture, and neither banner is one. The capture census after
75
+ * this is what draws that line: `waitForDisplaysDone` keys on the
76
+ * `data-display-drawn` the two comparative canvases publish from their stricter
77
+ * `settled` gate (see `comparativeReadiness` in `@jbrowse/synteny-core`, which
78
+ * holds both answers and says why an error separates them), and the census
79
+ * names every `data-display-phase="canceled"`.
80
+ */
81
+ const AppReadyMarker = observer(function AppReadyMarker({ session, }) {
82
+ const views = openViews(session);
83
+ const tracks = openTracks(session);
84
+ const loading = views.some(viewLoading) || tracks.some(trackLoading);
85
+ return (_jsx("span", { hidden: true, "data-testid": "app-ready-marker", "data-app-phase": loading ? 'loading' : 'ready', "data-app-views": session.views.length, "data-app-assemblies": JSON.stringify([
86
+ ...new Set(views.flatMap(v => v.assemblyNames ?? [])),
87
+ ]), "data-app-tracks": JSON.stringify(tracks.map(t => t.configuration.trackId ?? '(unnamed)')) }));
88
+ });
89
+ export default AppReadyMarker;
@@ -2,11 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
4
4
  import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
5
- import { readConfObject, readConfSlot } from '@jbrowse/core/configuration';
5
+ import { readConfObject } from '@jbrowse/core/configuration';
6
6
  import { CopyToClipboardButton } from '@jbrowse/core/ui';
7
7
  import { stripBaseUris } from '@jbrowse/core/util/addRelativeUris';
8
8
  import { makeStyles } from '@jbrowse/core/util/tss-react';
9
- import { getParent, hasParent, isStateTreeNode } from '@jbrowse/mobx-state-tree';
9
+ import { getParent, hasParent } from '@jbrowse/mobx-state-tree';
10
10
  import { Button } from '@mui/material';
11
11
  import { observer } from 'mobx-react';
12
12
  import RefNameAliasesDialog from './RefNameAliasesDialog.js';
@@ -39,15 +39,9 @@ const useStyles = makeStyles()(theme => ({
39
39
  * holding it *is* its assembly, the same walk `getConfAssemblyNames` makes.
40
40
  * Compared by identity against that node's `sequence`, so the card is the
41
41
  * assembly of the track the dialog is already showing.
42
- *
43
- * Both menus that open this dialog hand over a live node here: the hierarchical
44
- * selector reads `assemblyManager.get(name).configuration.sequence`, and the
45
- * in-view track label's `track.configuration` resolves to that same node
46
- * through `getTrackById`. The frozen `session.tracks` entry the rest of the
47
- * dialog has to cope with is never a reference sequence track.
48
42
  */
49
43
  function getAssemblyConf(config) {
50
- const parent = isStateTreeNode(config) && hasParent(config)
44
+ const parent = hasParent(config)
51
45
  ? getParent(config)
52
46
  : undefined;
53
47
  return parent?.sequence === config ? parent : undefined;
@@ -58,7 +52,7 @@ const AssemblyCard = observer(function AssemblyCard({ assemblyConf, session, hid
58
52
  const conf = readConfObject(assemblyConf);
59
53
  return (_jsxs(BaseCard, { title: "Assembly", children: [_jsxs("span", { className: classes.buttons, children: [_jsx(Button, { variant: "contained", color: "secondary", onClick: () => {
60
54
  setShowAliases(true);
61
- }, children: "Show ref name aliases" }), hideUris ? null : (_jsx(CopyToClipboardButton, { variant: "contained", value: () => JSON.stringify(stripBaseUris(structuredClone(conf)), null, 2), children: "Copy assembly config" }))] }), _jsx(Attributes, { attributes: conf, omit: hideFields, hideUris: hideUris }), showAliases ? (_jsx(RefNameAliasesDialog, { assemblyName: readConfSlot(assemblyConf, 'name'), session: session, onClose: () => {
55
+ }, children: "Show ref name aliases" }), hideUris ? null : (_jsx(CopyToClipboardButton, { variant: "contained", value: () => JSON.stringify(stripBaseUris(structuredClone(conf)), null, 2), children: "Copy assembly config" }))] }), _jsx(Attributes, { attributes: conf, omit: hideFields, hideUris: hideUris }), showAliases ? (_jsx(RefNameAliasesDialog, { assemblyName: readConfObject(assemblyConf, 'name'), session: session, onClose: () => {
62
56
  setShowAliases(false);
63
57
  } })) : null] }));
64
58
  });
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
3
+ import { readConfObject } from '@jbrowse/core/configuration';
3
4
  import { ErrorBanner, LoadingEllipses, SanitizedHTML } from '@jbrowse/core/ui';
4
5
  import { openLocation } from '@jbrowse/core/util/io';
5
6
  import { makeStyles } from '@jbrowse/core/util/tss-react';
@@ -18,7 +19,7 @@ const DescriptionCard = observer(function DescriptionCard({ url, }) {
18
19
  return (_jsxs(BaseCard, { title: "Description", children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", component: "div", className: classes.provenance, children: ["From the track hub, written for the UCSC Genome Browser \u2014 some display options it describes differ in JBrowse.", ' ', _jsx(Link, { href: url, target: "_blank", rel: "noopener noreferrer", children: "View original" })] }), error ? (_jsx(ErrorBanner, { error: error })) : isLoading || html === undefined ? (_jsx(LoadingEllipses, { message: "Loading description" })) : (_jsx(SanitizedHTML, { html: html }))] }));
19
20
  });
20
21
  const DescriptionPanel = observer(function DescriptionPanel({ config, }) {
21
- const url = getDescriptionHtmlUrl(config);
22
+ const url = getDescriptionHtmlUrl(readConfObject(config, 'metadata'));
22
23
  return url ? _jsx(DescriptionCard, { url: url }) : null;
23
24
  });
24
25
  export default DescriptionPanel;
@@ -1,26 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
3
3
  import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
4
- import { readConfSlot } from '@jbrowse/core/configuration';
4
+ import { readConfObject } from '@jbrowse/core/configuration';
5
5
  import { ErrorBanner, LoadingEllipses } from '@jbrowse/core/ui';
6
6
  import { statusProgressLabel } from '@jbrowse/core/util';
7
7
  import { useFetch } from '@jbrowse/core/util/useFetch';
8
8
  import { observer } from 'mobx-react';
9
- // Inline `observer(function(){})`, not a bare declaration: this reads
10
- // observables (session.rpcManager, and config slots off what is usually a live
11
- // MST node), and babel-plugin-react-compiler compiles the bare form and can
12
- // memoize such a read into staleness. RefNameInfoDialog beside it is written
13
- // the same way; see the React Compiler x MobX note in CLAUDE.md
14
9
  const FileInfoPanel = observer(function FileInfoPanel({ config, session, }) {
15
10
  const { rpcManager } = session;
16
- const trackId = readConfSlot(config, 'trackId');
11
+ const trackId = readConfObject(config, 'trackId');
17
12
  const { data: info, error, isLoading, status, } = useFetch(['CoreGetInfo', trackId],
18
13
  // reading a header can mean walking a v8 .hic's norm-vector index or a
19
14
  // multi-megabyte VCF header, so it gets both handles: closing the About
20
15
  // dialog stops it, and the wait says what it is
21
- async (_name, _trackId, stopToken, statusCallback) => (await rpcManager.call(trackId, 'CoreGetInfo', {
22
- adapterConfig: readConfSlot(config, 'adapter'),
23
- stopToken,
16
+ async (_name, _trackId, signal, statusCallback) => (await rpcManager.call(trackId, 'CoreGetInfo', {
17
+ adapterConfig: readConfObject(config, 'adapter'),
18
+ signal,
24
19
  statusCallback,
25
20
  })));
26
21
  const details = typeof info === 'string'
@@ -1,11 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { readConfSlot } from '@jbrowse/core/configuration';
2
+ import { readConfObject } from '@jbrowse/core/configuration';
3
3
  import { CopyToClipboardButton, Dialog, ErrorBanner, LoadingEllipses, } from '@jbrowse/core/ui';
4
4
  import { createStatusFanOut, statusProgressLabel } from '@jbrowse/core/util';
5
5
  import { getConfAssemblyNames } from '@jbrowse/core/util/tracks';
6
6
  import { makeStyles } from '@jbrowse/core/util/tss-react';
7
7
  import { useFetch } from '@jbrowse/core/util/useFetch';
8
- import { isStateTreeNode } from '@jbrowse/mobx-state-tree';
9
8
  import { DialogContent } from '@mui/material';
10
9
  import { observer } from 'mobx-react';
11
10
  const MAX_REF_NAMES = 10_000;
@@ -32,36 +31,24 @@ function formatRefNames(data, truncate) {
32
31
  .filter(s => s !== '')
33
32
  .join('\n');
34
33
  }
35
- /**
36
- * A `ReferenceSequenceTrack` config declares no `assemblyNames` slot at all —
37
- * `createReferenceSeqTrackConfig` omits it deliberately, because such a track's
38
- * assembly is the config node holding it. `getConfAssemblyNames` is the shared
39
- * resolver that walks to that parent, and reading the slot directly instead is
40
- * what left "Show ref names" on every reference sequence track loading forever:
41
- * `undefined` serialized into the fetch key, which `useFetch` reads as "don't
42
- * fetch". It throws when a config has neither, which is why this is called
43
- * inside the fetcher — an unanswerable question belongs in the error banner
44
- * rather than thrown out of a render.
45
- */
46
- function aboutAssemblyNames(config) {
47
- return isStateTreeNode(config)
48
- ? getConfAssemblyNames(config)
49
- : (readConfSlot(config, 'assemblyNames') ?? []);
50
- }
51
34
  const RefNameInfoDialog = observer(function RefNameInfoDialog({ config, session, onClose, }) {
52
35
  const { classes } = useStyles();
53
36
  const { rpcManager } = session;
54
- const trackId = readConfSlot(config, 'trackId');
55
- const { data, error, isLoading, status } = useFetch(['CoreGetRefNames', trackId], (_name, _trackId, stopToken, statusCallback) => {
37
+ const trackId = readConfObject(config, 'trackId');
38
+ const { data, error, isLoading, status } = useFetch(['CoreGetRefNames', trackId], (_name, _trackId, signal, statusCallback) => {
56
39
  // one status slot per assembly, so N concurrent reads aggregate into one
57
40
  // bar instead of the last writer winning
58
41
  const slot = createStatusFanOut(statusCallback);
59
- return Promise.all([...new Set(aboutAssemblyNames(config))].map(async (assemblyName) => [
42
+ // A `ReferenceSequenceTrack` config declares no `assemblyNames` slot: its
43
+ // assembly is the config node holding it, which `getConfAssemblyNames`
44
+ // walks to. It throws when a config has neither, and inside the fetcher
45
+ // that lands in the error banner rather than out of a render
46
+ return Promise.all([...new Set(getConfAssemblyNames(config))].map(async (assemblyName) => [
60
47
  assemblyName,
61
48
  await rpcManager.call(trackId, 'CoreGetRefNames', {
62
- adapterConfig: readConfSlot(config, 'adapter'),
49
+ adapterConfig: readConfObject(config, 'adapter'),
63
50
  assemblyName,
64
- stopToken,
51
+ signal,
65
52
  statusCallback: slot(),
66
53
  }),
67
54
  ]));