@jbrowse/core 5.0.0-beta.1 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/esm/BaseFeatureWidget/SequenceFeatureDetails/test_data/NCDN.d.ts +2 -2
- package/esm/PluginLoader.d.ts +1 -0
- package/esm/PluginLoader.js +52 -8
- package/esm/PluginManager.d.ts +70 -17
- package/esm/PluginManager.js +151 -20
- package/esm/ReExports/modules.js +3 -53
- package/esm/ReExports/publicUtil.d.ts +1 -1
- package/esm/ReExports/publicUtil.js +1 -1
- package/esm/ReExports/sharedModules.d.ts +96 -0
- package/esm/ReExports/sharedModules.js +59 -0
- package/esm/ReExports/uiStub.d.ts +1 -0
- package/esm/ReExports/uiStub.js +13 -0
- package/esm/ReExports/workerModules.d.ts +2 -0
- package/esm/ReExports/workerModules.js +10 -0
- package/esm/configuration/configurationSchema.d.ts +5 -38
- package/esm/configuration/configurationSchema.js +53 -40
- package/esm/configuration/promotableDefaults.js +12 -6
- package/esm/configuration/promotableResolve.d.ts +1 -1
- package/esm/configuration/promotableResolve.js +1 -1
- package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.d.ts +12 -0
- package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.js +39 -0
- package/esm/data_adapters/BaseAdapter/featureDensity.d.ts +29 -0
- package/esm/data_adapters/BaseAdapter/featureDensity.js +17 -0
- package/esm/data_adapters/BaseAdapter/index.d.ts +1 -0
- package/esm/data_adapters/BaseAdapter/index.js +1 -0
- package/esm/data_adapters/BaseAdapter/stats.d.ts +33 -1
- package/esm/data_adapters/BaseAdapter/stats.js +52 -13
- package/esm/data_adapters/BaseAdapter/types.d.ts +1 -0
- package/esm/pluggableElementTypes/DisplayType.d.ts +9 -10
- package/esm/pluggableElementTypes/DisplayType.js +43 -8
- package/esm/pluggableElementTypes/ViewType.d.ts +25 -2
- package/esm/pluggableElementTypes/ViewType.js +75 -2
- package/esm/pluggableElementTypes/extendElementType.js +8 -3
- package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +41 -0
- package/esm/pluggableElementTypes/models/BaseDisplayModel.js +74 -1
- package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +21 -0
- package/esm/pluggableElementTypes/models/BaseTrackModel.js +37 -2
- package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +49 -0
- package/esm/pluggableElementTypes/models/BaseViewModel.js +39 -0
- package/esm/pluggableElementTypes/models/assertDisplayContract.d.ts +10 -7
- package/esm/pluggableElementTypes/models/assertDisplayContract.js +23 -30
- package/esm/rpc/RpcRegistry.d.ts +10 -0
- package/esm/rpc/byteBudget.d.ts +9 -0
- package/esm/rpc/coreRpcMethods.d.ts +1 -0
- package/esm/rpc/coreRpcMethods.js +1 -0
- package/esm/rpc/methods/CoreGetFeatureDensity.d.ts +11 -0
- package/esm/rpc/methods/CoreGetFeatureDensity.js +22 -0
- package/esm/svg/svgReady.d.ts +1 -0
- package/esm/ui/CascadingMenu.js +30 -8
- package/esm/ui/LoadingOverlay.d.ts +14 -0
- package/esm/ui/LoadingOverlay.js +1 -1
- package/esm/ui/MenuTypes.d.ts +11 -2
- package/esm/ui/MenuTypes.js +3 -0
- package/esm/ui/PaletteContext.d.ts +11 -4
- package/esm/ui/PaletteContext.js +34 -5
- package/esm/ui/PinAdornment.js +12 -6
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/esm/ui/launchViewMenu.js +7 -1
- package/esm/ui/legendMenuItem.d.ts +5 -5
- package/esm/ui/legendMenuItem.js +5 -5
- package/esm/ui/makeSizeMenu.js +1 -1
- package/esm/ui/menuItems.d.ts +1 -1
- package/esm/ui/menuItems.js +1 -1
- package/esm/ui/promotableMenuItems.js +8 -6
- package/esm/ui/promotablePinCoverage.js +2 -1
- package/esm/ui/toggleMenuItems.d.ts +1 -3
- package/esm/util/addAndShowTrack.d.ts +1 -1
- package/esm/util/addAndShowTrack.js +5 -1
- package/esm/util/addTrackFromWidget.js +2 -2
- package/esm/util/cacheBudgets.d.ts +22 -19
- package/esm/util/cacheBudgets.js +22 -19
- package/esm/util/createStopTokenRotation.d.ts +1 -0
- package/esm/util/createStopTokenRotation.js +7 -27
- package/esm/util/fetchContext.d.ts +2 -2
- package/esm/util/fetchContext.js +2 -2
- package/esm/util/fetchPhases.d.ts +9 -9
- package/esm/util/hooks.d.ts +8 -0
- package/esm/util/hooks.js +27 -0
- package/esm/util/index.d.ts +2 -3
- package/esm/util/index.js +2 -3
- package/esm/util/installFetch.d.ts +49 -23
- package/esm/util/installFetch.js +49 -21
- package/esm/util/installInitAutorun.d.ts +8 -8
- package/esm/util/installInitAutorun.js +15 -15
- package/esm/util/lazyAfterAttach.d.ts +13 -0
- package/esm/util/lazyAfterAttach.js +31 -0
- package/esm/util/legendCandidates.d.ts +50 -0
- package/esm/util/legendCandidates.js +87 -0
- package/esm/util/mstUtils.d.ts +12 -1
- package/esm/util/mstUtils.js +20 -2
- package/esm/util/pluginStore.d.ts +1 -0
- package/esm/util/pluginStore.js +8 -1
- package/esm/util/range.d.ts +13 -9
- package/esm/util/range.js +17 -8
- package/esm/util/rxjs.d.ts +16 -0
- package/esm/util/rxjs.js +49 -0
- package/esm/util/tabix.d.ts +10 -4
- package/esm/util/tabix.js +11 -5
- package/esm/util/tracks.d.ts +48 -0
- package/esm/util/tracks.js +192 -66
- package/esm/util/types/index.d.ts +48 -5
- package/esm/util/types/index.js +3 -0
- package/esm/util/unknownSnapshotKeys.d.ts +36 -0
- package/esm/util/unknownSnapshotKeys.js +69 -0
- package/esm/util/viewStatus.d.ts +23 -0
- package/esm/util/viewStatus.js +17 -0
- package/esm/util/whenViewSettled.d.ts +17 -4
- package/esm/util/whenViewSettled.js +19 -6
- package/esm/util/withLaunchInput.d.ts +123 -0
- package/esm/util/withLaunchInput.js +228 -0
- package/package.json +12 -9
- package/esm/util/applyInitSettings.d.ts +0 -79
- package/esm/util/applyInitSettings.js +0 -91
package/esm/util/tracks.js
CHANGED
|
@@ -8,9 +8,9 @@ export { getFileName } from './getFileName.js';
|
|
|
8
8
|
// it. `fetchContext.ts` takes it from mstUtils instead, which is what keeps the
|
|
9
9
|
// configuration schemas this file pulls in out of the fetch harness's graph.
|
|
10
10
|
export { getRpcSessionId } from './parentWalk.js';
|
|
11
|
-
import { getParent, getSnapshot, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
|
|
11
|
+
import { getParent, getSnapshot, hasParent, isAlive, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
|
|
12
12
|
import { observable, runInAction, untracked } from 'mobx';
|
|
13
|
-
import {
|
|
13
|
+
import { readConfObject } from '../configuration/index.js';
|
|
14
14
|
import { getFileHandle, storeFileHandle, verifyPermission, } from './fileHandleStore.js';
|
|
15
15
|
import { getFileName } from './getFileName.js';
|
|
16
16
|
import { getContainingView, getEnv, getSession, objectHash, } from './mstUtils.js';
|
|
@@ -76,12 +76,22 @@ export function isSameAssemblyName(a, b, assemblyManager) {
|
|
|
76
76
|
* `ReferenceSequenceTrack` does not — its schema omits the slot deliberately
|
|
77
77
|
* (see `createReferenceSeqTrackConfig`), because the assembly config holding it
|
|
78
78
|
* IS its assembly, so the name is read off the parent instead.
|
|
79
|
+
*
|
|
80
|
+
* That makes a sequence track's assembly **positional**, so a copy of one that
|
|
81
|
+
* is not inside its assembly cannot answer at all — and a detached config is
|
|
82
|
+
* what every hydration path produces (`hydrateTrackConfig`, and the working
|
|
83
|
+
* copy a non-admin's edits would go to, ADR-032). `hasParent` is the guard:
|
|
84
|
+
* without it the walk raises MST's "Failed to find the parent" from a helper
|
|
85
|
+
* whose other two failure modes are a plain `undefined`.
|
|
79
86
|
*/
|
|
80
87
|
function confAssemblyNames(conf) {
|
|
81
88
|
const trackAssemblyNames = readConfObject(conf, 'assemblyNames');
|
|
82
89
|
if (trackAssemblyNames) {
|
|
83
90
|
return trackAssemblyNames;
|
|
84
91
|
}
|
|
92
|
+
if (!hasParent(conf)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
85
95
|
const parent = getParent(conf);
|
|
86
96
|
return 'sequence' in parent
|
|
87
97
|
? [readConfObject(parent, 'name')]
|
|
@@ -368,8 +378,12 @@ export function guessAdapter(file, index, adapterHint, model) {
|
|
|
368
378
|
}
|
|
369
379
|
export function guessTrackType(adapterType, model, file) {
|
|
370
380
|
if (model) {
|
|
371
|
-
|
|
372
|
-
|
|
381
|
+
// getEnv(model), not getEnv(getSession(model)): the env is tree-wide, so
|
|
382
|
+
// the session hop only narrowed what may be passed here — a session itself
|
|
383
|
+
// has no session ancestor, so guessAdapter above accepted one and this
|
|
384
|
+
// threw on it
|
|
385
|
+
const { pluginManager } = getEnv(model);
|
|
386
|
+
const trackTypeGuesser = pluginManager.evaluateExtensionPoint(
|
|
373
387
|
/** #extensionPoint Core-guessTrackTypeForLocation | sync | Guess a track type from a file location */
|
|
374
388
|
'Core-guessTrackTypeForLocation', () => undefined);
|
|
375
389
|
const trackType = trackTypeGuesser(adapterType, file);
|
|
@@ -392,24 +406,35 @@ export function guessTrackType(adapterType, model, file) {
|
|
|
392
406
|
*/
|
|
393
407
|
export function guessTrackConf(input, pluginManager, assemblyName) {
|
|
394
408
|
const { uri, index, ...extra } = typeof input === 'string' ? { uri: input } : input;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
409
|
+
return {
|
|
410
|
+
...guessTrackConfForLocation({ uri, locationType: 'UriLocation' }, index ? { uri: index, locationType: 'UriLocation' } : undefined, pluginManager, assemblyName),
|
|
411
|
+
...extra,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* {@link guessTrackConf} over an already-built {@link FileLocation}, for a
|
|
416
|
+
* caller holding a location the loose `{ uri }` vocabulary cannot spell — a
|
|
417
|
+
* desktop local path. Kept separate so `LooseTrackInput` stays URI-only: it
|
|
418
|
+
* sits on the track-config union and the root config's `tracks`, where a
|
|
419
|
+
* local path would be a config a web deployment could not open.
|
|
420
|
+
*
|
|
421
|
+
* The `trackId` is a content hash of the adapter, not a timestamp, so adding
|
|
422
|
+
* the same file twice is idempotent — `addToSession` dedupes on it.
|
|
423
|
+
*/
|
|
424
|
+
export function guessTrackConfForLocation(file, index, pluginManager, assemblyName) {
|
|
399
425
|
const adapterGuesser = pluginManager.evaluateExtensionPoint('Core-guessAdapterForLocation', () => undefined);
|
|
400
|
-
const adapter = adapterGuesser(file,
|
|
426
|
+
const adapter = adapterGuesser(file, index, undefined);
|
|
427
|
+
const name = getFileName(file);
|
|
401
428
|
if (!adapter || adapter.type === UNKNOWN) {
|
|
402
|
-
throw new Error(`could not infer a track type from "${
|
|
429
|
+
throw new Error(`could not infer a track type from "${name}"; pass a full track config instead`);
|
|
403
430
|
}
|
|
404
431
|
const trackTypeGuesser = pluginManager.evaluateExtensionPoint('Core-guessTrackTypeForLocation', () => undefined);
|
|
405
|
-
const name = getFileName(file);
|
|
406
432
|
return {
|
|
407
433
|
trackId: `${name}-${objectHash(adapter).slice(0, 8)}`,
|
|
408
434
|
type: trackTypeGuesser(adapter.type, file) ?? 'FeatureTrack',
|
|
409
435
|
name,
|
|
410
436
|
assemblyNames: assemblyName ? [assemblyName] : [],
|
|
411
437
|
adapter,
|
|
412
|
-
...extra,
|
|
413
438
|
};
|
|
414
439
|
}
|
|
415
440
|
/**
|
|
@@ -572,6 +597,57 @@ export function normalizeTrackInit(t) {
|
|
|
572
597
|
};
|
|
573
598
|
}
|
|
574
599
|
}
|
|
600
|
+
function resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf) {
|
|
601
|
+
const { pluginManager } = getEnv(self);
|
|
602
|
+
const session = getSession(self);
|
|
603
|
+
// `trackId` seeds the expansion because `guessTrackConf` otherwise
|
|
604
|
+
// synthesizes one from the filename, and the dedupe in the callers and
|
|
605
|
+
// `hideTrackGeneric` both look the track up by the id the caller passed.
|
|
606
|
+
// Only for the loose form: spreading a live MST node (SvInspector's
|
|
607
|
+
// circular track) would flatten it out of the `isStateTreeNode` branch.
|
|
608
|
+
const inlineRawConf = isLooseTrackConfig(inlineConf)
|
|
609
|
+
? expandLooseTrackConfig({ trackId, ...inlineConf }, pluginManager)
|
|
610
|
+
: inlineConf;
|
|
611
|
+
const rawConf = inlineRawConf ?? session.getTrackById(trackId);
|
|
612
|
+
if (!rawConf) {
|
|
613
|
+
throw new Error(`Could not resolve identifier "${trackId}"`);
|
|
614
|
+
}
|
|
615
|
+
const confSnapshot = structuredClone(isStateTreeNode(rawConf) ? getSnapshot(rawConf) : rawConf);
|
|
616
|
+
const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot);
|
|
617
|
+
const trackType = pluginManager.getTrackType(conf.type);
|
|
618
|
+
// Validate only a conf that is not already in the tree. `configSchema.create`
|
|
619
|
+
// here builds an entire config node and throws it away purely to surface a
|
|
620
|
+
// nice error; its preProcessSnapshot also re-runs Core-preProcessTrackConfig
|
|
621
|
+
// on a snapshot this function just preprocessed. A conf that is a state tree
|
|
622
|
+
// node was validated when MST created it, so all of that is pure waste on
|
|
623
|
+
// the common showTrack path.
|
|
624
|
+
if (!isStateTreeNode(rawConf)) {
|
|
625
|
+
try {
|
|
626
|
+
trackType.configSchema.create(conf, getEnv(self));
|
|
627
|
+
}
|
|
628
|
+
catch (e) {
|
|
629
|
+
throw new Error(`Track "${trackId}" has an invalid configuration: ${e}`, {
|
|
630
|
+
cause: e,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// A track container that isn't itself a view — a synteny level, which owns
|
|
635
|
+
// a track list but has no width and no registered view type — takes the
|
|
636
|
+
// display choice from the view it sits in, which is the same node
|
|
637
|
+
// getContainingView already resolves to for everything else beneath it.
|
|
638
|
+
const view = isViewModel(self) ? self : getContainingView(self);
|
|
639
|
+
const viewType = pluginManager.getViewType(view.type);
|
|
640
|
+
const picked = pickDisplayForView({
|
|
641
|
+
declaredDisplays: conf.displays ?? [],
|
|
642
|
+
requestedType: displayInitialSnapshot.type,
|
|
643
|
+
trackDisplayTypes: trackType.displayTypes.map(d => d.name),
|
|
644
|
+
viewDisplayTypes: viewType.displayTypes.map(d => d.name),
|
|
645
|
+
});
|
|
646
|
+
if (!picked) {
|
|
647
|
+
throw new Error(`Could not find a compatible display for view type ${view.type}`);
|
|
648
|
+
}
|
|
649
|
+
return { rawConf, conf, trackType, picked };
|
|
650
|
+
}
|
|
575
651
|
export function showTrackGeneric(self, trackId,
|
|
576
652
|
// `object`, annotated, rather than inferred from the default. A bare `{}`
|
|
577
653
|
// accepts every non-nullish value, numbers included, which is how a synteny
|
|
@@ -597,49 +673,28 @@ inlineConf) {
|
|
|
597
673
|
// snackbars. Config is validated before the push so the open set never holds
|
|
598
674
|
// a broken track.
|
|
599
675
|
try {
|
|
600
|
-
const rawConf = (
|
|
601
|
-
session.getTrackById(trackId);
|
|
602
|
-
if (!rawConf) {
|
|
603
|
-
throw new Error(`Could not resolve identifier "${trackId}"`);
|
|
604
|
-
}
|
|
605
|
-
const confSnapshot = structuredClone(isStateTreeNode(rawConf) ? getSnapshot(rawConf) : rawConf);
|
|
606
|
-
const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot);
|
|
607
|
-
const trackType = pluginManager.getTrackType(conf.type);
|
|
608
|
-
// Validate only a conf that is not already in the tree. `configSchema.create`
|
|
609
|
-
// here builds an entire config node and throws it away purely to surface a
|
|
610
|
-
// nice error; its preProcessSnapshot also re-runs Core-preProcessTrackConfig
|
|
611
|
-
// on a snapshot this function just preprocessed. A conf that is a state tree
|
|
612
|
-
// node was validated when MST created it, so all of that is pure waste on
|
|
613
|
-
// the common showTrack path.
|
|
614
|
-
if (!isStateTreeNode(rawConf)) {
|
|
615
|
-
try {
|
|
616
|
-
trackType.configSchema.create(conf, getEnv(self));
|
|
617
|
-
}
|
|
618
|
-
catch (e) {
|
|
619
|
-
throw new Error(`Track "${trackId}" has an invalid configuration: ${e}`, { cause: e });
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
// A track container that isn't itself a view — a synteny level, which owns
|
|
623
|
-
// a track list but has no width and no registered view type — takes the
|
|
624
|
-
// display choice from the view it sits in, which is the same node
|
|
625
|
-
// getContainingView already resolves to for everything else beneath it.
|
|
626
|
-
const view = isViewModel(self) ? self : getContainingView(self);
|
|
627
|
-
const viewType = pluginManager.getViewType(view.type);
|
|
628
|
-
const picked = pickDisplayForView({
|
|
629
|
-
declaredDisplays: conf.displays ?? [],
|
|
630
|
-
requestedType: displayInitialSnapshot.type,
|
|
631
|
-
trackDisplayTypes: trackType.displayTypes.map(d => d.name),
|
|
632
|
-
viewDisplayTypes: viewType.displayTypes.map(d => d.name),
|
|
633
|
-
});
|
|
634
|
-
if (!picked) {
|
|
635
|
-
throw new Error(`Could not find a compatible display for view type ${view.type}`);
|
|
636
|
-
}
|
|
676
|
+
const { rawConf, conf, trackType, picked } = resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf);
|
|
637
677
|
const { type: displayType, conf: displayConf } = picked;
|
|
638
678
|
const displayId = displayConf?.displayId ?? `${trackId}-${displayType}`;
|
|
679
|
+
// a registered display whose model is not loaded yet: the load re-enters
|
|
680
|
+
// through the view's own showTrack action, so a synchronous caller gets the
|
|
681
|
+
// track a tick late; a caller that needs it back awaits launchTrack
|
|
682
|
+
const displayRecord = pluginManager.resolveDisplayTypeRecord(displayType);
|
|
683
|
+
if (displayRecord && !displayRecord.isStateModelLoaded) {
|
|
684
|
+
void launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
639
687
|
const track = trackType.stateModel.create({
|
|
640
688
|
...initialSnapshot,
|
|
641
689
|
type: conf.type,
|
|
642
|
-
|
|
690
|
+
// `rawConf`, not the caller's `inlineConf`: the loose `{ trackId, uri }`
|
|
691
|
+
// form has no `type` for the config schema to dispatch on, and the
|
|
692
|
+
// concrete schema this lands on carries no loose preprocessor — that one
|
|
693
|
+
// is on `pluggableConfigSchemaType('track')`, which a track model's
|
|
694
|
+
// `ConfigurationReference` is not. Not `conf` either: that has already
|
|
695
|
+
// run Core-preProcessTrackConfig, which the schema's own
|
|
696
|
+
// preProcessSnapshot runs again.
|
|
697
|
+
configuration: inlineConf ? rawConf : trackId,
|
|
643
698
|
displays: [
|
|
644
699
|
{
|
|
645
700
|
...displayConf,
|
|
@@ -652,23 +707,30 @@ inlineConf) {
|
|
|
652
707
|
self.tracks.push(track);
|
|
653
708
|
// Display settings (height, color, …) are config slots now, not display
|
|
654
709
|
// instance props — passed in the display snapshot they'd be dropped as
|
|
655
|
-
// unknown MST keys.
|
|
656
|
-
// display config so they take effect and
|
|
657
|
-
//
|
|
710
|
+
// unknown MST keys. applyDisplaySettings routes the ones that are real
|
|
711
|
+
// slots onto the persistent display config so they take effect and
|
|
712
|
+
// survive hide/retick (#5591), running the schema's preProcessSnapshot
|
|
713
|
+
// first so shorthand expansions and legacy-key migrations reach this path
|
|
714
|
+
// too — a session spec, share link, or embed writes slots here rather
|
|
715
|
+
// than creating a config from a snapshot, and those surfaces are meant to
|
|
716
|
+
// speak the same vocabulary. Runs after the push so the display's config
|
|
717
|
+
// reference can resolve. Slots only, deliberately: this is the
|
|
718
|
+
// declarative surface, and `allowSetters` stays off so a spec key cannot
|
|
719
|
+
// reach internal display actions.
|
|
658
720
|
//
|
|
659
|
-
//
|
|
660
|
-
// and
|
|
661
|
-
//
|
|
662
|
-
// snapshot
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
721
|
+
// `type` is dropped first because this function already consumed it
|
|
722
|
+
// (pickDisplayForView above), and only `failed` — a key whose write threw —
|
|
723
|
+
// is notified: `unapplied` here also holds the MST display props the
|
|
724
|
+
// snapshot spread above already applied, so notifying on it would report a
|
|
725
|
+
// correct call as broken. A throwing slot value used to reach the outer
|
|
726
|
+
// catch and raise a snackbar; per-key catching kept the track intact but
|
|
727
|
+
// took the message away with it.
|
|
728
|
+
const displaySettings = Object.fromEntries(Object.entries(displayInitialSnapshot).filter(([key]) => key !== 'type'));
|
|
729
|
+
const report = track.applyDisplaySettings(displaySettings);
|
|
730
|
+
if (report.failed.length) {
|
|
731
|
+
session.notifyError(`Track "${trackId}" rejected ${report.failed
|
|
732
|
+
.map(f => `${f.key}: ${f.error}`)
|
|
733
|
+
.join('; ')}`);
|
|
672
734
|
}
|
|
673
735
|
// if this track came from a connection, persist its config so it survives
|
|
674
736
|
// reload without re-establishing the connection (no-op otherwise)
|
|
@@ -708,6 +770,32 @@ export function toggleTrackGeneric(self, trackId) {
|
|
|
708
770
|
? false
|
|
709
771
|
: !!showTrackGeneric(self, trackId);
|
|
710
772
|
}
|
|
773
|
+
export async function launchTrackGeneric(self, trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
|
|
774
|
+
const { pluginManager } = getEnv(self);
|
|
775
|
+
const session = getSession(self);
|
|
776
|
+
const found = self.tracks.find(t => t.configuration.trackId === trackId);
|
|
777
|
+
if (found) {
|
|
778
|
+
return found;
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
const { picked } = resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf);
|
|
782
|
+
await pluginManager.resolveDisplayTypeRecord(picked.type)?.loadStateModel();
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
session.notifyError(`${e}`, e);
|
|
786
|
+
return undefined;
|
|
787
|
+
}
|
|
788
|
+
if (!isAlive(self)) {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
return self.showTrack?.(trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
|
|
792
|
+
}
|
|
793
|
+
// the hide runs in the synchronous prologue, so it keeps action context
|
|
794
|
+
export async function launchToggleTrackGeneric(self, trackId) {
|
|
795
|
+
return hideTrackGeneric(self, trackId)
|
|
796
|
+
? false
|
|
797
|
+
: !!(await launchTrackGeneric(self, trackId));
|
|
798
|
+
}
|
|
711
799
|
/**
|
|
712
800
|
* Every track config the session can show, connection-supplied ones included.
|
|
713
801
|
*
|
|
@@ -726,3 +814,41 @@ export function allSessionTracks(session) {
|
|
|
726
814
|
? [...session.tracks, ...connectionTracks]
|
|
727
815
|
: session.tracks;
|
|
728
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* The session's annotation for `assemblyName`, as trackIds: what a view opened
|
|
819
|
+
* on another genome starts with. Only FeatureTracks, not everything configured
|
|
820
|
+
* for the assembly — an alignments track turned on behind the user's back
|
|
821
|
+
* fetches data nobody asked for, and a FeatureTrack over a few kb is one tabix
|
|
822
|
+
* range query.
|
|
823
|
+
*/
|
|
824
|
+
export function annotationTrackIds(session, assemblyName) {
|
|
825
|
+
return allSessionTracks(session).flatMap(track => readConfObject(track, 'type') === 'FeatureTrack' &&
|
|
826
|
+
readConfObject(track, 'assemblyNames').some(name => isSameAssemblyName(name, assemblyName, session.assemblyManager))
|
|
827
|
+
? [readConfObject(track, 'trackId')]
|
|
828
|
+
: []);
|
|
829
|
+
}
|
|
830
|
+
function isNavigableView(view) {
|
|
831
|
+
return 'navToLocString' in view;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* One assembly on its own in a linear genome view, at `loc`. Keyed on `id`:
|
|
835
|
+
* following several alignments to one genome re-navigates the view rather than
|
|
836
|
+
* stacking a new one each time.
|
|
837
|
+
*/
|
|
838
|
+
export async function openAssemblyInLinearView({ session, id, assemblyName, loc, tracks, }) {
|
|
839
|
+
const view = session.views.find(v => v.id === id);
|
|
840
|
+
if (view && isNavigableView(view)) {
|
|
841
|
+
await view.navToLocString(loc, assemblyName);
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
session.addView('LinearGenomeView', {
|
|
845
|
+
id,
|
|
846
|
+
assembly: assemblyName,
|
|
847
|
+
loc,
|
|
848
|
+
tracks,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
export function openMateLabel(assemblyName) {
|
|
853
|
+
return `Open ${assemblyName} at the matching region`;
|
|
854
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ViewSnapshotInput } from '../../PluginManager.ts';
|
|
1
2
|
import type TextSearchManager from '../../TextSearch/TextSearchManager.ts';
|
|
2
3
|
import type { AnyConfigurationModel, ResolvableDisplay } from '../../configuration/index.ts';
|
|
3
4
|
import type { BaseInternetAccountModel } from '../../pluggableElementTypes/models/index.ts';
|
|
@@ -22,7 +23,8 @@ export type { AugmentedRegion, BasePlugin, BlobLocation, FileHandleLocation, Fil
|
|
|
22
23
|
export interface AbstractViewContainer extends IStateTreeNode {
|
|
23
24
|
views: AbstractViewModel[];
|
|
24
25
|
removeView(view: AbstractViewModel): void;
|
|
25
|
-
addView(typeName:
|
|
26
|
+
addView<N extends string>(typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): AbstractViewModel;
|
|
27
|
+
launchView(typeName: string, initialState?: Record<string, unknown>): Promise<AbstractViewModel>;
|
|
26
28
|
}
|
|
27
29
|
export declare function isViewContainer(thing: unknown): thing is AbstractViewContainer;
|
|
28
30
|
/**
|
|
@@ -32,6 +34,7 @@ export declare function isViewContainer(thing: unknown): thing is AbstractViewCo
|
|
|
32
34
|
*/
|
|
33
35
|
export interface TrackActionView {
|
|
34
36
|
showTrack: (id: string) => void;
|
|
37
|
+
launchTrack: (id: string) => Promise<unknown>;
|
|
35
38
|
getActiveDisplayId?: (trackId: string) => string | undefined;
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
@@ -153,6 +156,21 @@ export interface SessionWithConfigEditing extends AbstractSessionModel {
|
|
|
153
156
|
}): void;
|
|
154
157
|
}
|
|
155
158
|
export declare function isSessionModelWithConfigEditing(t: unknown): t is SessionWithConfigEditing;
|
|
159
|
+
/**
|
|
160
|
+
* abstract interface for a session that keeps a list of views, so that `addView`
|
|
161
|
+
* appends and the view a launch came out of survives it.
|
|
162
|
+
*
|
|
163
|
+
* `AbstractViewContainer` alone does not say this. The single-view embedded
|
|
164
|
+
* products satisfy it with a `views` getter that wraps their one `view` in a
|
|
165
|
+
* fresh plain array and an `addView` that overwrites that view, so "Open in new
|
|
166
|
+
* view" there would destroy the view it was offered from. A view list that is
|
|
167
|
+
* itself part of the state tree is what tells the two apart: only a session
|
|
168
|
+
* that stores its views can hold one more.
|
|
169
|
+
*/
|
|
170
|
+
export interface SessionWithMultipleViews extends AbstractSessionModel {
|
|
171
|
+
views: AbstractViewModel[] & IStateTreeNode;
|
|
172
|
+
}
|
|
173
|
+
export declare function isSessionWithMultipleViews(t: unknown): t is SessionWithMultipleViews;
|
|
156
174
|
/**
|
|
157
175
|
* abstract interface for a session that can swap one of its views for a new one
|
|
158
176
|
* of another type, in the slot the old view occupied.
|
|
@@ -164,7 +182,7 @@ export declare function isSessionModelWithConfigEditing(t: unknown): t is Sessio
|
|
|
164
182
|
* and the guard is what a launcher asks before offering it.
|
|
165
183
|
*/
|
|
166
184
|
export interface SessionWithViewReplacement extends AbstractSessionModel {
|
|
167
|
-
replaceView(view: AbstractViewModel, typeName:
|
|
185
|
+
replaceView<N extends string>(view: AbstractViewModel, typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): AbstractViewModel;
|
|
168
186
|
}
|
|
169
187
|
export declare function isSessionWithViewReplacement(t: unknown): t is SessionWithViewReplacement;
|
|
170
188
|
/**
|
|
@@ -188,10 +206,10 @@ export declare function canReplaceView(session: AbstractViewContainer, view: Abs
|
|
|
188
206
|
* that can't replace a view falls back to appending, so a caller passes the
|
|
189
207
|
* source view unconditionally and never has to ask twice.
|
|
190
208
|
*/
|
|
191
|
-
export declare function addOrReplaceView({ session, typeName, initialState, replacing, }: {
|
|
209
|
+
export declare function addOrReplaceView<N extends string>({ session, typeName, initialState, replacing, }: {
|
|
192
210
|
session: AbstractViewContainer;
|
|
193
|
-
typeName:
|
|
194
|
-
initialState?:
|
|
211
|
+
typeName: N;
|
|
212
|
+
initialState?: NoInfer<ViewSnapshotInput<N>>;
|
|
195
213
|
replacing?: AbstractViewModel;
|
|
196
214
|
}): AbstractViewModel;
|
|
197
215
|
/**
|
|
@@ -371,6 +389,7 @@ export declare function isSessionWithSessionPlugins(thing: unknown): thing is Se
|
|
|
371
389
|
*/
|
|
372
390
|
export interface SessionWithPermanentPlugins extends AbstractSessionModel {
|
|
373
391
|
permanentPlugins: PluginDefinition[];
|
|
392
|
+
permanentPluginsSkipped: boolean;
|
|
374
393
|
addPermanentPlugin: (plugin: PluginDefinition) => void;
|
|
375
394
|
removePermanentPlugin: (plugin: PluginDefinition) => void;
|
|
376
395
|
}
|
|
@@ -410,6 +429,13 @@ export interface TrackContainer {
|
|
|
410
429
|
* silence.
|
|
411
430
|
*/
|
|
412
431
|
showTrack: (trackId: string, initialSnapshot?: object, displayInitialSnapshot?: Record<string, unknown>, inlineConf?: Record<string, unknown>) => unknown;
|
|
432
|
+
/**
|
|
433
|
+
* The sync pair still works on an unloaded display — it loads and shows a
|
|
434
|
+
* tick later, returning undefined — so these are for a caller that needs
|
|
435
|
+
* the track back.
|
|
436
|
+
*/
|
|
437
|
+
launchTrack: (trackId: string, initialSnapshot?: object, displayInitialSnapshot?: Record<string, unknown>, inlineConf?: Record<string, unknown>) => Promise<unknown>;
|
|
438
|
+
launchToggleTrack: (trackId: string) => Promise<unknown>;
|
|
413
439
|
hideTrack: (trackId: string) => unknown;
|
|
414
440
|
toggleTrack: (trackId: string) => unknown;
|
|
415
441
|
}
|
|
@@ -453,6 +479,23 @@ export interface AbstractViewModel {
|
|
|
453
479
|
* `trackContainerFor` cannot answer that — a walker has no id to ask with.
|
|
454
480
|
*/
|
|
455
481
|
trackContainers?: TrackContainer[];
|
|
482
|
+
/**
|
|
483
|
+
* every track open on this view itself — its own `tracks` and any containers
|
|
484
|
+
* it owns instead; tracks on nested views are excluded. Derived by
|
|
485
|
+
* `BaseViewModel`, so a view composing the base answers correctly without
|
|
486
|
+
* implementing anything. Required, like `bodyMounted`: a stand-in that
|
|
487
|
+
* forgets it would silently under-report instead of failing the build.
|
|
488
|
+
*/
|
|
489
|
+
ownTracks: AbstractTrackModel[];
|
|
490
|
+
/**
|
|
491
|
+
* this view and every view nested inside it, to any depth. The walk over
|
|
492
|
+
* `tracks` / `trackContainers` / `views` is written once, in `BaseViewModel`;
|
|
493
|
+
* anything needing "everything on screen" asks this and `ownTracks` rather
|
|
494
|
+
* than knowing which property a container keeps its children on.
|
|
495
|
+
*/
|
|
496
|
+
allViews: AbstractViewModel[];
|
|
497
|
+
/** every track open on this view or any view nested inside it */
|
|
498
|
+
allTracks: AbstractTrackModel[];
|
|
456
499
|
}
|
|
457
500
|
export declare function isViewModel(thing: unknown): thing is AbstractViewModel;
|
|
458
501
|
type Display = {
|
package/esm/util/types/index.js
CHANGED
|
@@ -18,6 +18,9 @@ export function isSessionModel(thing) {
|
|
|
18
18
|
export function isSessionModelWithConfigEditing(t) {
|
|
19
19
|
return isSessionModel(t) && 'editConfiguration' in t;
|
|
20
20
|
}
|
|
21
|
+
export function isSessionWithMultipleViews(t) {
|
|
22
|
+
return isSessionModel(t) && isStateTreeNode(t.views);
|
|
23
|
+
}
|
|
21
24
|
export function isSessionWithViewReplacement(t) {
|
|
22
25
|
return isSessionModel(t) && 'replaceView' in t;
|
|
23
26
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
/** what a diagnostic calls the view: its snapshot `type`, or the model name */
|
|
3
|
+
export declare function viewLabel(self: IStateTreeNode): string;
|
|
4
|
+
/**
|
|
5
|
+
* What every surface calls this mistake. A session spec launches a view without
|
|
6
|
+
* ever building a snapshot, so it classifies its own keys and reports them
|
|
7
|
+
* through its own session rather than through the sink below — one wording, so
|
|
8
|
+
* a typo reads the same whether it was written in a config or in a URL.
|
|
9
|
+
*/
|
|
10
|
+
export declare function unknownKeysMessage(label: string, keys: string[]): string;
|
|
11
|
+
/**
|
|
12
|
+
* Name the keys a view was handed and could not place, from the partition
|
|
13
|
+
* `withLaunchInput` runs on the snapshot.
|
|
14
|
+
*/
|
|
15
|
+
export declare function reportUnknownKeys(self: IStateTreeNode, keys: string[]): void;
|
|
16
|
+
/**
|
|
17
|
+
* Name the row lists a view refused whole. `withLaunchInput` splits an authored
|
|
18
|
+
* array into the recipes a launcher resolves and the built snapshots MST
|
|
19
|
+
* restores, per entry — but a row list is indexed against the view's `levels`
|
|
20
|
+
* and per-level tracks, so a list holding both kinds has no correct split and
|
|
21
|
+
* is refused rather than renumbered.
|
|
22
|
+
*/
|
|
23
|
+
export declare function reportMalformedRows(self: IStateTreeNode, keys: string[]): void;
|
|
24
|
+
/**
|
|
25
|
+
* What every surface calls v4's nested `init`. v5 takes every setting directly
|
|
26
|
+
* on the view object and unwraps the nesting on the way in, so the settings
|
|
27
|
+
* still arrive — this names the spelling to fix while they do.
|
|
28
|
+
*/
|
|
29
|
+
export declare function legacyInitMessage(label: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Name a snapshot that still writes v4's nested `init`. The console alone,
|
|
32
|
+
* unlike the two reporters above: the settings were applied, so there is
|
|
33
|
+
* nothing for a visitor to act on and a warning toast over a working view is
|
|
34
|
+
* noise. `jbrowse validate` is where the author of the config hears about it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function reportLegacyInit(self: IStateTreeNode): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getType } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
import { getNotificationSink } from './sessionServices.js';
|
|
3
|
+
/** what a diagnostic calls the view: its snapshot `type`, or the model name */
|
|
4
|
+
export function viewLabel(self) {
|
|
5
|
+
return self.type ?? getType(self).name;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* What every surface calls this mistake. A session spec launches a view without
|
|
9
|
+
* ever building a snapshot, so it classifies its own keys and reports them
|
|
10
|
+
* through its own session rather than through the sink below — one wording, so
|
|
11
|
+
* a typo reads the same whether it was written in a config or in a URL.
|
|
12
|
+
*/
|
|
13
|
+
export function unknownKeysMessage(label, keys) {
|
|
14
|
+
return `${label} ignored unknown key(s): ${keys.join(', ')}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Name the keys a view was handed and could not place, from the partition
|
|
18
|
+
* `withLaunchInput` runs on the snapshot.
|
|
19
|
+
*/
|
|
20
|
+
export function reportUnknownKeys(self, keys) {
|
|
21
|
+
if (!keys.length) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const message = unknownKeysMessage(viewLabel(self), keys);
|
|
25
|
+
console.warn(message);
|
|
26
|
+
try {
|
|
27
|
+
getNotificationSink(self).notify(message, 'warning');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// a view built outside a session has nowhere to put it
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Name the row lists a view refused whole. `withLaunchInput` splits an authored
|
|
35
|
+
* array into the recipes a launcher resolves and the built snapshots MST
|
|
36
|
+
* restores, per entry — but a row list is indexed against the view's `levels`
|
|
37
|
+
* and per-level tracks, so a list holding both kinds has no correct split and
|
|
38
|
+
* is refused rather than renumbered.
|
|
39
|
+
*/
|
|
40
|
+
export function reportMalformedRows(self, keys) {
|
|
41
|
+
if (!keys.length) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const message = `${viewLabel(self)} refused ${keys.join(', ')}: the list mixes built view snapshots with recipes to open one, and the rows index against the levels between them. Write all of them one way.`;
|
|
45
|
+
console.warn(message);
|
|
46
|
+
try {
|
|
47
|
+
getNotificationSink(self).notify(message, 'warning');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// a view built outside a session has nowhere to put it
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What every surface calls v4's nested `init`. v5 takes every setting directly
|
|
55
|
+
* on the view object and unwraps the nesting on the way in, so the settings
|
|
56
|
+
* still arrive — this names the spelling to fix while they do.
|
|
57
|
+
*/
|
|
58
|
+
export function legacyInitMessage(label) {
|
|
59
|
+
return `${label} nests its settings under "init", which is deprecated: write every setting directly on the view object.`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Name a snapshot that still writes v4's nested `init`. The console alone,
|
|
63
|
+
* unlike the two reporters above: the settings were applied, so there is
|
|
64
|
+
* nothing for a visitor to act on and a warning toast over a working view is
|
|
65
|
+
* noise. `jbrowse validate` is where the author of the config hears about it.
|
|
66
|
+
*/
|
|
67
|
+
export function reportLegacyInit(self) {
|
|
68
|
+
console.warn(legacyInitMessage(viewLabel(self)));
|
|
69
|
+
}
|
package/esm/util/viewStatus.d.ts
CHANGED
|
@@ -56,3 +56,26 @@ export interface ViewStatusInputs {
|
|
|
56
56
|
* with nothing anywhere saying why. `noRegions` is that state, named.
|
|
57
57
|
*/
|
|
58
58
|
export declare function computeViewStatus({ error, hasSomethingToShow, loading, }: ViewStatusInputs): ViewStatus;
|
|
59
|
+
/**
|
|
60
|
+
* The slice of `AssemblyManager` this needs, spelled out rather than imported:
|
|
61
|
+
* the real type is an MST model a `PluginManager` built, so naming it here
|
|
62
|
+
* would cost this leaf the whole application's type graph (`util/CLAUDE.md`).
|
|
63
|
+
*/
|
|
64
|
+
interface AssemblyErrorSource {
|
|
65
|
+
get: (name: string) => {
|
|
66
|
+
error?: unknown;
|
|
67
|
+
} | undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The assembly failures behind a view's `assemblyNames`, joined, or `undefined`
|
|
71
|
+
* when there are none.
|
|
72
|
+
*
|
|
73
|
+
* **Undefined and not `''`**, which is the whole reason this is shared. Three
|
|
74
|
+
* views spelled it as a bare `.join(', ')`, and a getter that answers the empty
|
|
75
|
+
* string for "nothing failed" reads as an error to every `!== undefined` test —
|
|
76
|
+
* `DotplotView.error` folded it in with `??`, so the view reported a terminal
|
|
77
|
+
* error permanently and jbrowse-img refused to render any dotplot. The in-app
|
|
78
|
+
* readers all happened to be truthiness tests, which is why it stayed invisible.
|
|
79
|
+
*/
|
|
80
|
+
export declare function assemblyErrorMessage(assemblyManager: AssemblyErrorSource, assemblyNames: string[]): string | undefined;
|
|
81
|
+
export {};
|
package/esm/util/viewStatus.js
CHANGED
|
@@ -18,3 +18,20 @@ export function computeViewStatus({ error, hasSomethingToShow, loading, }) {
|
|
|
18
18
|
const pending = loading();
|
|
19
19
|
return pending ? { type: 'loading', ...pending } : { type: 'ready' };
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The assembly failures behind a view's `assemblyNames`, joined, or `undefined`
|
|
23
|
+
* when there are none.
|
|
24
|
+
*
|
|
25
|
+
* **Undefined and not `''`**, which is the whole reason this is shared. Three
|
|
26
|
+
* views spelled it as a bare `.join(', ')`, and a getter that answers the empty
|
|
27
|
+
* string for "nothing failed" reads as an error to every `!== undefined` test —
|
|
28
|
+
* `DotplotView.error` folded it in with `??`, so the view reported a terminal
|
|
29
|
+
* error permanently and jbrowse-img refused to render any dotplot. The in-app
|
|
30
|
+
* readers all happened to be truthiness tests, which is why it stayed invisible.
|
|
31
|
+
*/
|
|
32
|
+
export function assemblyErrorMessage(assemblyManager, assemblyNames) {
|
|
33
|
+
const errors = assemblyNames
|
|
34
|
+
.map(name => assemblyManager.get(name)?.error)
|
|
35
|
+
.filter(e => !!e);
|
|
36
|
+
return errors.length > 0 ? errors.join(', ') : undefined;
|
|
37
|
+
}
|