@jbrowse/product-core 4.2.1 → 5.0.0-beta.1

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 (133) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +629 -304
  2. package/esm/RootModel/BaseRootModel.js +138 -14
  3. package/esm/RootModel/HierarchicalConfig.d.ts +57 -21
  4. package/esm/RootModel/HierarchicalConfig.js +36 -0
  5. package/esm/RootModel/InternetAccounts.d.ts +18 -1
  6. package/esm/RootModel/InternetAccounts.js +120 -22
  7. package/esm/RootModel/PreferencesConfig.d.ts +53 -0
  8. package/esm/RootModel/PreferencesConfig.js +61 -0
  9. package/esm/RootModel/createConfigModel.d.ts +232 -0
  10. package/esm/RootModel/createConfigModel.js +56 -0
  11. package/esm/RootModel/index.d.ts +10 -5
  12. package/esm/RootModel/index.js +10 -5
  13. package/esm/RootModel/menuItems.d.ts +24 -0
  14. package/esm/RootModel/menuItems.js +143 -0
  15. package/esm/Session/BaseSession.d.ts +310 -11
  16. package/esm/Session/BaseSession.js +448 -14
  17. package/esm/Session/Connections.d.ts +114 -8
  18. package/esm/Session/Connections.js +195 -74
  19. package/esm/Session/DrawerWidgets.d.ts +115 -12
  20. package/esm/Session/DrawerWidgets.js +161 -42
  21. package/esm/Session/MultipleViews.d.ts +1266 -1128
  22. package/esm/Session/MultipleViews.js +357 -66
  23. package/esm/Session/Preferences.d.ts +1665 -0
  24. package/esm/Session/Preferences.js +54 -0
  25. package/esm/Session/ReferenceManagement.d.ts +28 -3
  26. package/esm/Session/ReferenceManagement.js +56 -41
  27. package/esm/Session/SessionTracks.d.ts +1275 -1138
  28. package/esm/Session/SessionTracks.js +532 -35
  29. package/esm/Session/Themes.d.ts +87 -2
  30. package/esm/Session/Themes.js +156 -23
  31. package/esm/Session/TrackMenu.d.ts +112 -0
  32. package/esm/Session/TrackMenu.js +200 -0
  33. package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
  34. package/esm/Session/TrackMenuSessionMixin.js +41 -0
  35. package/esm/Session/Tracks.d.ts +1169 -1117
  36. package/esm/Session/Tracks.js +160 -20
  37. package/esm/Session/assertSessionModel.d.ts +21 -0
  38. package/esm/Session/assertSessionModel.js +1 -0
  39. package/esm/Session/finalizeSession.d.ts +16 -0
  40. package/esm/Session/finalizeSession.js +29 -0
  41. package/esm/Session/index.d.ts +22 -10
  42. package/esm/Session/index.js +13 -10
  43. package/esm/Session/shareableSnapshot.d.ts +77 -0
  44. package/esm/Session/shareableSnapshot.js +196 -0
  45. package/esm/Session/temporaryAssemblyTracks.d.ts +23 -0
  46. package/esm/Session/temporaryAssemblyTracks.js +38 -0
  47. package/esm/assertExtends.d.ts +55 -0
  48. package/esm/assertExtends.js +1 -0
  49. package/esm/controllerTracks.d.ts +56 -0
  50. package/esm/controllerTracks.js +77 -0
  51. package/esm/destroyViewState.d.ts +21 -0
  52. package/esm/destroyViewState.js +29 -0
  53. package/esm/index.d.ts +31 -4
  54. package/esm/index.js +31 -4
  55. package/esm/loadPlugins.d.ts +33 -0
  56. package/esm/loadPlugins.js +38 -0
  57. package/esm/localFiles.d.ts +74 -0
  58. package/esm/localFiles.js +156 -0
  59. package/esm/observeSession.d.ts +56 -0
  60. package/esm/observeSession.js +91 -0
  61. package/esm/pluginInput.d.ts +32 -0
  62. package/esm/pluginInput.js +19 -0
  63. package/esm/pruneUnbuildableNodes.d.ts +109 -0
  64. package/esm/pruneUnbuildableNodes.js +568 -0
  65. package/esm/resolveAssemblies.d.ts +44 -0
  66. package/esm/resolveAssemblies.js +55 -0
  67. package/esm/rpcWorker.d.ts +26 -0
  68. package/esm/rpcWorker.js +96 -15
  69. package/esm/scheduleDetachedDestroy.d.ts +18 -0
  70. package/esm/scheduleDetachedDestroy.js +24 -0
  71. package/esm/sessionMigrations/index.d.ts +53 -0
  72. package/esm/sessionMigrations/index.js +432 -0
  73. package/esm/sessionUrl.d.ts +41 -0
  74. package/esm/sessionUrl.js +53 -0
  75. package/esm/sessionUtils.d.ts +69 -0
  76. package/esm/sessionUtils.js +613 -0
  77. package/esm/siblingCast.d.ts +14 -0
  78. package/esm/siblingCast.js +16 -0
  79. package/esm/snapshotUtils.d.ts +8 -0
  80. package/esm/snapshotUtils.js +12 -0
  81. package/esm/ui/AboutDialog.d.ts +3 -6
  82. package/esm/ui/AboutDialog.js +4 -4
  83. package/esm/ui/AboutDialogContents.d.ts +2 -6
  84. package/esm/ui/AboutDialogContents.js +34 -13
  85. package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
  86. package/esm/ui/AssemblyInfoPanel.js +69 -0
  87. package/esm/ui/DisplayDefaultsSection.d.ts +36 -0
  88. package/esm/ui/DisplayDefaultsSection.js +65 -0
  89. package/esm/ui/Drawer.d.ts +21 -0
  90. package/esm/ui/Drawer.js +60 -0
  91. package/esm/ui/DrawerControls.d.ts +5 -0
  92. package/esm/ui/DrawerControls.js +28 -0
  93. package/esm/ui/DrawerHeader.d.ts +6 -0
  94. package/esm/ui/DrawerHeader.js +36 -0
  95. package/esm/ui/DrawerWidget.d.ts +5 -0
  96. package/esm/ui/DrawerWidget.js +12 -0
  97. package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
  98. package/esm/ui/DrawerWidgetSelector.js +40 -0
  99. package/esm/ui/FileInfoPanel.d.ts +3 -6
  100. package/esm/ui/FileInfoPanel.js +35 -26
  101. package/esm/ui/HeaderButtons.d.ts +7 -1
  102. package/esm/ui/HeaderButtons.js +23 -18
  103. package/esm/ui/ModalWidget.d.ts +12 -0
  104. package/esm/ui/ModalWidget.js +33 -0
  105. package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
  106. package/esm/ui/ModalWidgetAppBar.js +9 -0
  107. package/esm/ui/PreferencesDialog.d.ts +56 -0
  108. package/esm/ui/PreferencesDialog.js +145 -0
  109. package/esm/ui/PreferencesResetDialog.d.ts +8 -0
  110. package/esm/ui/PreferencesResetDialog.js +19 -0
  111. package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
  112. package/esm/ui/RefNameAliasesDialog.js +65 -0
  113. package/esm/ui/RefNameInfoDialog.d.ts +3 -6
  114. package/esm/ui/RefNameInfoDialog.js +57 -63
  115. package/esm/ui/WidgetBody.d.ts +18 -0
  116. package/esm/ui/WidgetBody.js +22 -0
  117. package/esm/ui/WidgetHeading.d.ts +7 -0
  118. package/esm/ui/WidgetHeading.js +7 -0
  119. package/esm/ui/drawerLayout.d.ts +18 -0
  120. package/esm/ui/drawerLayout.js +22 -0
  121. package/esm/ui/index.d.ts +13 -1
  122. package/esm/ui/index.js +12 -1
  123. package/esm/ui/util.d.ts +37 -4
  124. package/esm/ui/util.js +35 -38
  125. package/esm/useEngineLifecycle.d.ts +13 -0
  126. package/esm/useEngineLifecycle.js +22 -0
  127. package/package.json +11 -15
  128. package/esm/RootModel/FormatAbout.d.ts +0 -12
  129. package/esm/RootModel/FormatAbout.js +0 -15
  130. package/esm/RootModel/FormatDetails.d.ts +0 -24
  131. package/esm/RootModel/FormatDetails.js +0 -27
  132. package/esm/Session/DialogQueue.d.ts +0 -17
  133. package/esm/Session/DialogQueue.js +0 -31
@@ -1,26 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { CopyToClipboardButton } from '@jbrowse/core/ui';
3
+ import { pluralize } from '@jbrowse/core/util';
4
+ import { stripBaseUris } from '@jbrowse/core/util/addRelativeUris';
3
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
- import { Button } from '@mui/material';
5
- import { removeAttr } from "./util.js";
6
- const useStyles = makeStyles()({
6
+ import { Button, Tooltip, Typography } from '@mui/material';
7
+ const useStyles = makeStyles()(theme => ({
7
8
  button: {
8
9
  float: 'right',
9
10
  },
10
- });
11
- function HeaderButtons({ conf, setShowRefNames }) {
12
- const [copied, setCopied] = useState(false);
11
+ // JSX drops the whitespace between two elements on separate lines, so the two
12
+ // contained buttons rendered edge to edge, reading as one two-tone control
13
+ buttons: {
14
+ display: 'flex',
15
+ gap: theme.spacing(1),
16
+ justifyContent: 'flex-end',
17
+ },
18
+ note: {
19
+ clear: 'both',
20
+ display: 'block',
21
+ textAlign: 'right',
22
+ },
23
+ }));
24
+ function HeaderButtons({ conf, hideUris, fromDisplayTypeDefaults = [], setShowRefNames, }) {
13
25
  const { classes } = useStyles();
14
- return (_jsxs("span", { className: classes.button, children: [_jsx(Button, { variant: "contained", color: "secondary", onClick: () => {
15
- setShowRefNames(true);
16
- }, children: "Show ref names" }), _jsx(Button, { variant: "contained", onClick: async () => {
17
- const { default: copy } = await import('copy-to-clipboard');
18
- const snap = removeAttr(structuredClone(conf), 'baseUri');
19
- copy(JSON.stringify(snap, null, 2));
20
- setCopied(true);
21
- setTimeout(() => {
22
- setCopied(false);
23
- }, 1000);
24
- }, children: copied ? 'Copied to clipboard!' : 'Copy config' })] }));
26
+ const n = fromDisplayTypeDefaults.length;
27
+ return (_jsxs("span", { className: classes.button, children: [_jsxs("span", { className: classes.buttons, children: [_jsx(Button, { variant: "contained", color: "secondary", onClick: () => {
28
+ setShowRefNames(true);
29
+ }, children: "Show ref names" }), hideUris ? null : (_jsx(CopyToClipboardButton, { variant: "contained", value: () => JSON.stringify(stripBaseUris(structuredClone(conf)), null, 2), children: "Copy config" }))] }), hideUris || !n ? null : (_jsx(Tooltip, { title: fromDisplayTypeDefaults.join(', '), children: _jsxs(Typography, { variant: "caption", color: "textSecondary", className: classes.note, children: ["includes ", n, " ", pluralize(n, 'setting'), " from your session-wide defaults"] }) }))] }));
25
30
  }
26
31
  export default HeaderButtons;
@@ -0,0 +1,12 @@
1
+ import type { SessionWithWidgets } from '@jbrowse/core/util';
2
+ /**
3
+ * Renders the session's visible widget in a modal dialog rather than a drawer.
4
+ * `onClose` is caller-owned: in the app the modal is a pop-out of the drawer and
5
+ * closing returns the widget to it, while in embedded products the modal is the
6
+ * only widget surface and closing must dismiss the widget outright.
7
+ */
8
+ declare const ModalWidget: ({ session, onClose, }: {
9
+ session: SessionWithWidgets;
10
+ onClose: () => void;
11
+ }) => import("react").JSX.Element | null;
12
+ export default ModalWidget;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Dialog } from '@jbrowse/core/ui';
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
4
+ import { getEnv } from '@jbrowse/mobx-state-tree';
5
+ import { Paper } from '@mui/material';
6
+ import { observer } from 'mobx-react';
7
+ import ModalWidgetAppBar from './ModalWidgetAppBar.js';
8
+ import WidgetBody from './WidgetBody.js';
9
+ const useStyles = makeStyles()({
10
+ paper: {
11
+ overflow: 'auto',
12
+ // the track selector's fab positions against this
13
+ position: 'relative',
14
+ width: 800,
15
+ maxWidth: '100%',
16
+ // matches the drawer's sense of "tall but not full screen", and unlike a
17
+ // window.innerHeight read it keeps up with a window resize
18
+ height: '62.5vh',
19
+ },
20
+ });
21
+ /**
22
+ * Renders the session's visible widget in a modal dialog rather than a drawer.
23
+ * `onClose` is caller-owned: in the app the modal is a pop-out of the drawer and
24
+ * closing returns the widget to it, while in embedded products the modal is the
25
+ * only widget surface and closing must dismiss the widget outright.
26
+ */
27
+ const ModalWidget = observer(function ModalWidget({ session, onClose, }) {
28
+ const { classes } = useStyles();
29
+ const { visibleWidget } = session;
30
+ const { pluginManager } = getEnv(session);
31
+ return visibleWidget ? (_jsx(Dialog, { open: true, onClose: onClose, maxWidth: "xl", header: _jsx(ModalWidgetAppBar, { widget: visibleWidget, pluginManager: pluginManager, onClose: onClose }), children: _jsx(Paper, { className: classes.paper, children: _jsx(WidgetBody, { session: session, widget: visibleWidget }) }) })) : null;
32
+ });
33
+ export default ModalWidget;
@@ -0,0 +1,8 @@
1
+ import type PluginManager from '@jbrowse/core/PluginManager';
2
+ import type { Widget } from '@jbrowse/core/util';
3
+ declare const ModalWidgetAppBar: ({ widget, pluginManager, onClose, }: {
4
+ widget: Widget;
5
+ pluginManager: PluginManager;
6
+ onClose: () => void;
7
+ }) => import("react").JSX.Element;
8
+ export default ModalWidgetAppBar;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import CloseIcon from '@mui/icons-material/Close';
3
+ import { AppBar, Box, IconButton, Toolbar } from '@mui/material';
4
+ import { observer } from 'mobx-react';
5
+ import WidgetHeading from './WidgetHeading.js';
6
+ const ModalWidgetAppBar = observer(function ModalWidgetAppBar({ widget, pluginManager, onClose, }) {
7
+ return (_jsx(AppBar, { position: "static", children: _jsxs(Toolbar, { children: [_jsx(WidgetHeading, { widget: widget, pluginManager: pluginManager }), _jsx(Box, { sx: { flexGrow: 1 } }), _jsx(IconButton, { color: "inherit", onClick: onClose, "data-testid": "modal-close", children: _jsx(CloseIcon, {}) })] }) }));
8
+ });
9
+ export default ModalWidgetAppBar;
@@ -0,0 +1,56 @@
1
+ import type PluginManager from '@jbrowse/core/PluginManager';
2
+ import type { ThemeMap } from '@jbrowse/core/ui';
3
+ import type { AnimationMode, TrackConfigChange } from '@jbrowse/core/util';
4
+ import type React from 'react';
5
+ export interface PreferencesDialogSession {
6
+ allThemes: () => ThemeMap;
7
+ themeName?: string;
8
+ setThemeName: (arg: string) => void;
9
+ stickyViewHeaders: boolean;
10
+ setStickyViewHeaders: (sticky: boolean) => void;
11
+ effectiveUseWorkspaces: boolean;
12
+ defaultUseWorkspaces: boolean;
13
+ setUseWorkspacesPreference: (useWorkspaces: boolean) => void;
14
+ resetUseWorkspaces: () => void;
15
+ animationMode: AnimationMode;
16
+ numberGrouping: boolean;
17
+ scrollZoom: boolean;
18
+ setScrollZoom: (flag: boolean) => void;
19
+ setPreferenceOverride: (key: string, value: unknown) => void;
20
+ clearPreferenceOverrides: () => void;
21
+ getPreferenceChanges: () => TrackConfigChange[];
22
+ resetPreferenceChange: (path: string[]) => void;
23
+ getDisplayTypeDefaults: () => {
24
+ displayType: string;
25
+ slot: string;
26
+ value: unknown;
27
+ }[];
28
+ setDisplayTypeDefault: (displayType: string, slot: string, value: unknown) => void;
29
+ }
30
+ /**
31
+ * Descriptor returned from the `Core-preferencesDialogPanels` extension point.
32
+ * Each panel renders as its own labeled section in the dialog.
33
+ */
34
+ export interface PreferencesPanelDescriptor {
35
+ name: string;
36
+ Component: React.ComponentType<{
37
+ session: PreferencesDialogSession;
38
+ }>;
39
+ }
40
+ declare module '@jbrowse/core/PluginManager' {
41
+ interface ExtensionPointRegistry {
42
+ 'Core-preferencesDialogPanels': {
43
+ args: PreferencesPanelDescriptor[];
44
+ result: PreferencesPanelDescriptor[];
45
+ props: {
46
+ session: PreferencesDialogSession;
47
+ };
48
+ };
49
+ }
50
+ }
51
+ declare const PreferencesDialog: ({ handleClose, session, pluginManager, }: {
52
+ handleClose: () => void;
53
+ session: PreferencesDialogSession;
54
+ pluginManager: PluginManager;
55
+ }) => React.JSX.Element;
56
+ export default PreferencesDialog;
@@ -0,0 +1,145 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Suspense, useState } from 'react';
3
+ import { Dialog, LabeledCheckbox } from '@jbrowse/core/ui';
4
+ import { SCROLL_ZOOM_HELP, SCROLL_ZOOM_LABEL, } from '@jbrowse/core/ui/scrollZoomLabels';
5
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
6
+ import { Button, DialogActions, DialogContent, Divider, FormGroup, MenuItem, TextField, Typography, } from '@mui/material';
7
+ import { observer } from 'mobx-react';
8
+ import DisplayDefaultsSection from './DisplayDefaultsSection.js';
9
+ import PreferencesResetDialog from './PreferencesResetDialog.js';
10
+ const useStyles = makeStyles()({
11
+ container: {
12
+ width: 800,
13
+ },
14
+ panelHeading: {
15
+ marginTop: 16,
16
+ },
17
+ field: {
18
+ marginTop: 16,
19
+ display: 'block',
20
+ },
21
+ });
22
+ const PREFERENCE_SUBSYSTEMS = [
23
+ {
24
+ head: 'theme',
25
+ change: s => s.themeName && s.themeName !== 'default'
26
+ ? { path: ['theme'], from: 'default', to: s.themeName }
27
+ : undefined,
28
+ reset: s => {
29
+ s.setThemeName('default');
30
+ },
31
+ },
32
+ {
33
+ head: 'stickyViewHeaders',
34
+ change: s => s.stickyViewHeaders
35
+ ? undefined
36
+ : { path: ['stickyViewHeaders'], from: true, to: false },
37
+ reset: s => {
38
+ s.setStickyViewHeaders(true);
39
+ },
40
+ },
41
+ {
42
+ head: 'useWorkspaces',
43
+ // the resolved flag against the admin default, not the override map's view
44
+ // of it: a session-scoped value (a spec `layout`, "move view to a tab")
45
+ // writes no override, so the map reported nothing to reset while
46
+ // `resetUseWorkspaces` would plainly have turned workspaces back off —
47
+ // leaving the confirmation empty and its Reset button disabled
48
+ change: s => s.effectiveUseWorkspaces === s.defaultUseWorkspaces
49
+ ? undefined
50
+ : {
51
+ path: ['useWorkspaces'],
52
+ from: s.defaultUseWorkspaces,
53
+ to: s.effectiveUseWorkspaces,
54
+ },
55
+ reset: s => {
56
+ s.resetUseWorkspaces();
57
+ },
58
+ },
59
+ ];
60
+ // heads the subsystems above own outright: their row states the resolved value,
61
+ // so the session's override-map row for the same key would double-report it
62
+ // (and disagree with it whenever the two layers differ)
63
+ const SUBSYSTEM_HEADS = new Set(PREFERENCE_SUBSYSTEMS.map(p => p.head));
64
+ // every preference that currently differs from its default, so the confirmation
65
+ // dialog shows the full effect of a reset: the session override map plus each
66
+ // non-map subsystem above.
67
+ function collectPreferenceChanges(session) {
68
+ return [
69
+ ...session
70
+ .getPreferenceChanges()
71
+ .filter(c => !SUBSYSTEM_HEADS.has(c.path[0])),
72
+ ...PREFERENCE_SUBSYSTEMS.map(p => p.change(session)).filter(c => c !== undefined),
73
+ ];
74
+ }
75
+ // Reset every preference this dialog exposes back to its default: clear the
76
+ // whole override map (scrollZoom, animationMode, all promoted display-type
77
+ // defaults) at once, then reset each non-map subsystem through its own setter.
78
+ function resetAllPreferences(session) {
79
+ session.clearPreferenceOverrides();
80
+ for (const p of PREFERENCE_SUBSYSTEMS) {
81
+ p.reset(session);
82
+ }
83
+ }
84
+ // Revert a single change row (see `collectPreferenceChanges`) to its default:
85
+ // a non-map subsystem by matching `head`, everything else back through the
86
+ // session that produced the row. The override map's own path shapes are the
87
+ // session's business — it owns the composite-key layout a promoted
88
+ // display-type default is stored under, so it also owns undoing one.
89
+ function resetPreferenceChange(session, change) {
90
+ const subsystem = PREFERENCE_SUBSYSTEMS.find(p => p.head === change.path[0]);
91
+ if (subsystem) {
92
+ subsystem.reset(session);
93
+ }
94
+ else {
95
+ session.resetPreferenceChange(change.path);
96
+ }
97
+ }
98
+ // declarative user-preference rows backed by the session preferences-override
99
+ // system (BaseSession getPreference/setPreferenceOverride). Add a row here to
100
+ // surface a new preference; resolution + persistence are already handled.
101
+ const PREFERENCE_SELECTS = [
102
+ {
103
+ key: 'animationMode',
104
+ label: 'Animations',
105
+ options: [
106
+ { value: 'system', label: 'Follow system (reduced motion)' },
107
+ { value: 'enabled', label: 'Always on' },
108
+ { value: 'disabled', label: 'Off' },
109
+ ],
110
+ get: session => session.animationMode,
111
+ },
112
+ ];
113
+ const PreferencesDialog = observer(function PreferencesDialog({ handleClose, session, pluginManager, }) {
114
+ const { classes } = useStyles();
115
+ // a confirmation dialog (showing the exact diff) guards the destructive reset,
116
+ // instead of an accidental single click wiping every preference
117
+ const [resetDialogOpen, setResetDialogOpen] = useState(false);
118
+ const extraPanels = pluginManager.evaluateExtensionPoint(
119
+ /** #extensionPoint Core-preferencesDialogPanels | sync | Add panels to the preferences dialog */
120
+ 'Core-preferencesDialogPanels', [], { session });
121
+ return (_jsxs(Dialog, { title: "Preferences", open: true, onClose: handleClose, maxWidth: "xl", children: [_jsxs(DialogContent, { className: classes.container, children: [_jsx(TextField, { select: true, variant: "outlined", label: "Theme", value: session.themeName, onChange: event => {
122
+ session.setThemeName(event.target.value);
123
+ }, children: Object.entries(session.allThemes()).map(([key, val]) => (_jsx(MenuItem, { value: key, children: val.name || '(Unknown name)' }, key))) }), PREFERENCE_SELECTS.map(row => (_jsx(TextField, { select: true, variant: "outlined", className: classes.field, label: row.label, value: row.get(session), onChange: event => {
124
+ session.setPreferenceOverride(row.key, event.target.value);
125
+ }, children: row.options.map(opt => (_jsx(MenuItem, { value: opt.value, children: opt.label }, opt.value))) }, row.key))), _jsxs(FormGroup, { children: [_jsx(LabeledCheckbox, { checked: session.scrollZoom, label: `${SCROLL_ZOOM_LABEL}: ${SCROLL_ZOOM_HELP}`, onChange: checked => {
126
+ session.setScrollZoom(checked);
127
+ } }), _jsx(LabeledCheckbox, { checked: session.stickyViewHeaders, label: "Keep view header visible", onChange: checked => {
128
+ session.setStickyViewHeaders(checked);
129
+ } }), _jsx(LabeledCheckbox, { checked: session.effectiveUseWorkspaces, label: "Use workspaces (tabbed/tiled view layout)", onChange: checked => {
130
+ session.setUseWorkspacesPreference(checked);
131
+ } }), _jsx(LabeledCheckbox, { checked: session.numberGrouping, label: "Show thousand separators in numbers, e.g. chr1:1,234,567 vs chr1:1234567 (reload app/refresh browser page to apply)", onChange: checked => {
132
+ session.setPreferenceOverride('numberGrouping', checked);
133
+ } })] }), _jsx(Divider, {}), _jsx(Typography, { variant: "subtitle1", className: classes.panelHeading, children: "Display defaults" }), _jsx(DisplayDefaultsSection, { session: session, pluginManager: pluginManager }), extraPanels.map(({ name, Component }) => (_jsxs("div", { children: [_jsx(Divider, {}), _jsx(Typography, { variant: "subtitle1", className: classes.panelHeading, children: name }), _jsx(Suspense, { fallback: null, children: _jsx(Component, { session: session }) })] }, name)))] }), _jsxs(DialogActions, { children: [_jsx(Button, { variant: "contained", color: "secondary", onClick: () => {
134
+ setResetDialogOpen(true);
135
+ }, children: "Reset to defaults\u2026" }), _jsx(Button, { variant: "contained", onClick: () => {
136
+ handleClose();
137
+ }, children: "Close" })] }), resetDialogOpen ? (_jsx(PreferencesResetDialog, { changes: collectPreferenceChanges(session), onReset: () => {
138
+ resetAllPreferences(session);
139
+ }, onResetRow: change => {
140
+ resetPreferenceChange(session, change);
141
+ }, onClose: () => {
142
+ setResetDialogOpen(false);
143
+ } })) : null] }));
144
+ });
145
+ export default PreferencesDialog;
@@ -0,0 +1,8 @@
1
+ import type { TrackConfigChange } from '@jbrowse/core/util';
2
+ declare const PreferencesResetDialog: ({ changes, onReset, onResetRow, onClose, }: {
3
+ changes: TrackConfigChange[];
4
+ onReset: () => void;
5
+ onResetRow: (change: TrackConfigChange) => void;
6
+ onClose: () => void;
7
+ }) => import("react").JSX.Element;
8
+ export default PreferencesResetDialog;
@@ -0,0 +1,19 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SettingsChangesTable, SubmitDialog } from '@jbrowse/core/ui';
3
+ import { DialogContentText, Typography } from '@mui/material';
4
+ import { observer } from 'mobx-react';
5
+ // Confirmation shown before "Reset to defaults" wipes every customized
6
+ // preference. Lists exactly what will change as a Setting/Default/Current diff
7
+ // (the same table the per-track reset uses) so the reset is never a blind
8
+ // action. A standard modal — focus-trapped, Enter to confirm, Escape to cancel
9
+ // — replaces the former label-swapping two-click button, which screen readers
10
+ // and keyboard users could not follow.
11
+ const PreferencesResetDialog = observer(function PreferencesResetDialog({ changes, onReset, onResetRow, onClose, }) {
12
+ return (_jsx(SubmitDialog, { open: true, title: "Reset preferences to defaults", maxWidth: "md", submitText: "Reset to defaults", submitDisabled: changes.length === 0, cancelText: "Cancel", onCancel: () => {
13
+ onClose();
14
+ }, onSubmit: () => {
15
+ onReset();
16
+ onClose();
17
+ }, children: changes.length ? (_jsxs(_Fragment, { children: [_jsx(DialogContentText, { children: "These preferences differ from their defaults. Reset an individual entry with its revert button, or reset them all below:" }), _jsx(SettingsChangesTable, { changes: changes, onResetRow: onResetRow })] })) : (_jsx(Typography, { children: "All preferences are already at their defaults." })) }));
18
+ });
19
+ export default PreferencesResetDialog;
@@ -0,0 +1,9 @@
1
+ import type { AbstractSessionModel } from '@jbrowse/core/util';
2
+ export declare function formatRows(rows: [string, string[]][]): string;
3
+ export declare function alignRows(rows: [string, string[]][]): string;
4
+ declare const RefNameAliasesDialog: ({ assemblyName, session, onClose, }: {
5
+ assemblyName: string;
6
+ session: AbstractSessionModel;
7
+ onClose: () => void;
8
+ }) => import("react").JSX.Element;
9
+ export default RefNameAliasesDialog;
@@ -0,0 +1,65 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { CopyToClipboardButton, Dialog, ErrorBanner, LoadingEllipses, } from '@jbrowse/core/ui';
4
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
5
+ import { DialogContent, TextField, Typography } from '@mui/material';
6
+ import { observer } from 'mobx-react';
7
+ const MAX_ROWS = 10_000;
8
+ const MAX_NAME_COLUMN = 30;
9
+ const useStyles = makeStyles()(theme => ({
10
+ container: {
11
+ minWidth: 800,
12
+ },
13
+ rows: {
14
+ maxHeight: 300,
15
+ overflow: 'auto',
16
+ background: theme.palette.background.default,
17
+ },
18
+ header: {
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ gap: theme.spacing(2),
22
+ marginBottom: theme.spacing(1),
23
+ },
24
+ }));
25
+ // what the copy button puts on the clipboard: tab-separated, so it pastes into
26
+ // a spreadsheet as two columns
27
+ export function formatRows(rows) {
28
+ return rows
29
+ .map(([refName, names]) => `${refName}\t${names.slice(1).join(', ')}`)
30
+ .join('\n');
31
+ }
32
+ // what the dialog shows. A tab is 8 columns wide in a <pre>, so an assembly
33
+ // whose names straddle that (hg38's `chr7` and `chr7_GL383534v2_alt`) reads as
34
+ // two ragged columns; padding to the widest name on screen is one. Capped, or
35
+ // a single 60-character GenArk contig indents every alias off the right edge
36
+ export function alignRows(rows) {
37
+ let width = 0;
38
+ for (const [refName] of rows) {
39
+ width = Math.max(width, refName.length);
40
+ }
41
+ const pad = Math.min(width, MAX_NAME_COLUMN);
42
+ return rows
43
+ .map(([refName, names]) => `${refName.padEnd(pad)} ${names.slice(1).join(', ')}`)
44
+ .join('\n');
45
+ }
46
+ const RefNameAliasesDialog = observer(function RefNameAliasesDialog({ assemblyName, session, onClose, }) {
47
+ const { classes } = useStyles();
48
+ const [filter, setFilter] = useState('');
49
+ const assembly = session.assemblyManager.get(assemblyName);
50
+ // reading this is also what starts the load: assembly.afterAttach kicks
51
+ // load() when refNameAliases is first observed, so the dialog fills in for an
52
+ // assembly nothing has looked at yet instead of showing an empty table
53
+ const names = assembly?.namesByCanonicalRefName;
54
+ const query = filter.toLowerCase();
55
+ const rows = names
56
+ ? [...names].filter(([, group]) => group.some(name => name.toLowerCase().includes(query)))
57
+ : [];
58
+ const truncated = rows.length > MAX_ROWS;
59
+ return (_jsx(Dialog, { open: true, title: `Reference name aliases for ${assemblyName}`, onClose: () => {
60
+ onClose();
61
+ }, maxWidth: "xl", children: _jsx(DialogContent, { className: classes.container, children: assembly?.error ? (_jsx(ErrorBanner, { error: assembly.error })) : !assembly ? (_jsxs(Typography, { children: ["No assembly named ", assemblyName, " is loaded in this session"] })) : !names ? (_jsx(LoadingEllipses, { message: "Loading assembly" })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: classes.header, children: [_jsx(TextField, { label: "Filter", helperText: "Matches a reference name or any of its aliases", value: filter, onChange: event => {
62
+ setFilter(event.target.value);
63
+ } }), _jsx(CopyToClipboardButton, { variant: "contained", value: () => formatRows(rows), children: "Copy ref name aliases" })] }), _jsx("pre", { className: classes.rows, children: alignRows(truncated ? rows.slice(0, MAX_ROWS) : rows) }), truncated ? (_jsxs(Typography, { variant: "caption", children: ["Showing the first ", MAX_ROWS, " of ", rows.length, " reference names; filter, or use the copy button, for the rest"] })) : null] })) }) }));
64
+ });
65
+ export default RefNameAliasesDialog;
@@ -1,8 +1,5 @@
1
- import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
- import type { AbstractSessionModel } from '@jbrowse/core/util';
3
- declare const RefNameInfoDialog: ({ config, session, onClose, }: {
4
- config: AnyConfigurationModel | Record<string, unknown>;
5
- session: AbstractSessionModel;
1
+ import type { AboutPanelProps } from './util.ts';
2
+ declare const RefNameInfoDialog: ({ config, session, onClose, }: AboutPanelProps & {
6
3
  onClose: () => void;
7
- }) => import("react/jsx-runtime").JSX.Element;
4
+ }) => import("react").JSX.Element;
8
5
  export default RefNameInfoDialog;
@@ -1,10 +1,13 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { Dialog, ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { readConfSlot } from '@jbrowse/core/configuration';
3
+ import { CopyToClipboardButton, Dialog, ErrorBanner, LoadingEllipses, } from '@jbrowse/core/ui';
4
+ import { createStatusFanOut, statusProgressLabel } from '@jbrowse/core/util';
5
+ import { getConfAssemblyNames } from '@jbrowse/core/util/tracks';
4
6
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { Button, DialogContent } from '@mui/material';
7
+ import { useFetch } from '@jbrowse/core/util/useFetch';
8
+ import { isStateTreeNode } from '@jbrowse/mobx-state-tree';
9
+ import { DialogContent } from '@mui/material';
6
10
  import { observer } from 'mobx-react';
7
- import { readConf } from "./util.js";
8
11
  const MAX_REF_NAMES = 10_000;
9
12
  const useStyles = makeStyles()(theme => ({
10
13
  container: {
@@ -17,66 +20,57 @@ const useStyles = makeStyles()(theme => ({
17
20
  background: theme.palette.background.default,
18
21
  },
19
22
  }));
23
+ function formatRefNames(data, truncate) {
24
+ return data
25
+ .flatMap(([assemblyName, names]) => [
26
+ `--- ${assemblyName} ---`,
27
+ ...(truncate ? names.slice(0, MAX_REF_NAMES) : names),
28
+ truncate && names.length > MAX_REF_NAMES
29
+ ? `\nToo many refNames to show in browser for ${assemblyName}, use "Copy ref names" button to copy to clipboard`
30
+ : '',
31
+ ])
32
+ .filter(s => s !== '')
33
+ .join('\n');
34
+ }
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
+ }
20
51
  const RefNameInfoDialog = observer(function RefNameInfoDialog({ config, session, onClose, }) {
21
52
  const { classes } = useStyles();
22
- const [error, setError] = useState();
23
- const [refNames, setRefNames] = useState();
24
- const [copied, setCopied] = useState(false);
25
53
  const { rpcManager } = session;
26
- const trackId = readConf(config, 'trackId');
27
- const assemblyNames = readConf(config, 'assemblyNames');
28
- useEffect(() => {
29
- ;
30
- (async () => {
31
- try {
32
- const map = await Promise.all([...new Set(assemblyNames)].map(async (assemblyName) => {
33
- const adapterConfig = readConf(config, 'adapter');
34
- return [
35
- assemblyName,
36
- (await rpcManager.call(trackId, 'CoreGetRefNames', {
37
- adapterConfig,
38
- regions: [
39
- {
40
- assemblyName,
41
- },
42
- ],
43
- })),
44
- ];
45
- }));
46
- setRefNames(Object.fromEntries(map));
47
- }
48
- catch (e) {
49
- console.error(e);
50
- setError(e);
51
- }
52
- })();
53
- }, [config, rpcManager, trackId, assemblyNames]);
54
- const names = refNames ? Object.entries(refNames) : [];
55
- const result = names
56
- .flatMap(([assemblyName, refNames]) => {
57
- return [
58
- `--- ${assemblyName} ---`,
59
- ...refNames.slice(0, MAX_REF_NAMES),
60
- refNames.length > MAX_REF_NAMES
61
- ? `\nToo many refNames to show in browser for ${assemblyName}, use "Copy ref names" button to copy to clipboard`
62
- : '',
63
- ];
64
- })
65
- .filter(f => !!f)
66
- .join('\n');
67
- return (_jsx(Dialog, { open: true, title: "Reference sequence names used in track", onClose: onClose, maxWidth: "xl", children: _jsx(DialogContent, { className: classes.container, children: error ? (_jsx(ErrorMessage, { error: error })) : refNames === undefined ? (_jsx(LoadingEllipses, { message: "Loading refNames" })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "contained", onClick: async () => {
68
- const { default: copy } = await import('copy-to-clipboard');
69
- copy(names
70
- .flatMap(([assemblyName, refNames]) => [
71
- `--- ${assemblyName} ---`,
72
- ...refNames,
73
- ])
74
- .filter(f => !!f)
75
- .join('\n'));
76
- setCopied(true);
77
- setTimeout(() => {
78
- setCopied(false);
79
- }, 1000);
80
- }, children: copied ? 'Copied to clipboard!' : 'Copy ref names' }), _jsx("pre", { className: classes.refNames, children: result })] })) }) }));
54
+ const trackId = readConfSlot(config, 'trackId');
55
+ const { data, error, isLoading, status } = useFetch(['CoreGetRefNames', trackId], (_name, _trackId, stopToken, statusCallback) => {
56
+ // one status slot per assembly, so N concurrent reads aggregate into one
57
+ // bar instead of the last writer winning
58
+ const slot = createStatusFanOut(statusCallback);
59
+ return Promise.all([...new Set(aboutAssemblyNames(config))].map(async (assemblyName) => [
60
+ assemblyName,
61
+ await rpcManager.call(trackId, 'CoreGetRefNames', {
62
+ adapterConfig: readConfSlot(config, 'adapter'),
63
+ assemblyName,
64
+ stopToken,
65
+ statusCallback: slot(),
66
+ }),
67
+ ]));
68
+ });
69
+ // undefined here means the key was incomplete and the fetch never ran, not
70
+ // that one is still in flight — `isLoading` is what says that. Treating the
71
+ // two as the same thing is what left the dialog spinning with nothing behind
72
+ // it; an empty list at least says so
73
+ const refNames = data ?? [];
74
+ return (_jsx(Dialog, { open: true, title: "Reference sequence names used in track", onClose: onClose, maxWidth: "xl", children: _jsx(DialogContent, { className: classes.container, children: error ? (_jsx(ErrorBanner, { error: error })) : isLoading ? (_jsx(LoadingEllipses, { message: statusProgressLabel(status) || 'Loading refNames' })) : (_jsxs(_Fragment, { children: [_jsx(CopyToClipboardButton, { variant: "contained", value: () => formatRefNames(refNames, false), children: "Copy ref names" }), _jsx("pre", { className: classes.refNames, children: formatRefNames(refNames, true) })] })) }) }));
81
75
  });
82
76
  export default RefNameInfoDialog;
@@ -0,0 +1,18 @@
1
+ import type { SessionWithWidgets, Widget } from '@jbrowse/core/util';
2
+ /**
3
+ * Renders one widget: resolve its React component, run it through the
4
+ * `Core-replaceWidget` fold, and contain both its lazy chunk and its errors.
5
+ *
6
+ * Only the chrome around a widget differs between surfaces — the drawer's
7
+ * resizable paper and header, the modal's dialog — so this is the part all of
8
+ * them share, spelled once. The error boundary in particular was previously
9
+ * only on the drawer path: popping the same widget out to a modal turned a
10
+ * widget that throws during render from a banner in the widget's own area into
11
+ * an unhandled throw that took the app down with it.
12
+ */
13
+ declare const WidgetBody: ({ session, widget, toolbarHeight, }: {
14
+ session: SessionWithWidgets;
15
+ widget: Widget;
16
+ toolbarHeight?: number;
17
+ }) => import("react").JSX.Element;
18
+ export default WidgetBody;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense } from 'react';
3
+ import { ErrorBanner, LoadingEllipses, PluggableComponent, } from '@jbrowse/core/ui';
4
+ import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary';
5
+ import { getEnv } from '@jbrowse/core/util';
6
+ import { observer } from 'mobx-react';
7
+ /**
8
+ * Renders one widget: resolve its React component, run it through the
9
+ * `Core-replaceWidget` fold, and contain both its lazy chunk and its errors.
10
+ *
11
+ * Only the chrome around a widget differs between surfaces — the drawer's
12
+ * resizable paper and header, the modal's dialog — so this is the part all of
13
+ * them share, spelled once. The error boundary in particular was previously
14
+ * only on the drawer path: popping the same widget out to a modal turned a
15
+ * widget that throws during render from a banner in the widget's own area into
16
+ * an unhandled throw that took the app down with it.
17
+ */
18
+ const WidgetBody = observer(function WidgetBody({ session, widget, toolbarHeight = 0, }) {
19
+ const { pluginManager } = getEnv(session);
20
+ return (_jsx(Suspense, { fallback: _jsx(LoadingEllipses, {}), children: _jsx(ErrorBoundary, { FallbackComponent: ({ error }) => _jsx(ErrorBanner, { error: error }), children: _jsx(PluggableComponent, { pluginManager: pluginManager, name: "Core-replaceWidget", component: pluginManager.getWidgetType(widget.type).ReactComponent, props: { model: widget, session, toolbarHeight } }) }) }));
21
+ });
22
+ export default WidgetBody;
@@ -0,0 +1,7 @@
1
+ import type PluginManager from '@jbrowse/core/PluginManager';
2
+ import type { Widget } from '@jbrowse/core/util';
3
+ declare function WidgetHeading({ widget, pluginManager, }: {
4
+ widget: Widget;
5
+ pluginManager: PluginManager;
6
+ }): import("react").JSX.Element;
7
+ export default WidgetHeading;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Typography } from '@mui/material';
3
+ function WidgetHeading({ widget, pluginManager, }) {
4
+ const { HeadingComponent, heading } = pluginManager.getWidgetType(widget.type);
5
+ return HeadingComponent ? (_jsx(HeadingComponent, { model: widget })) : (_jsx(Typography, { variant: "h6", color: "inherit", children: heading }));
6
+ }
7
+ export default WidgetHeading;