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

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 (53) hide show
  1. package/esm/RootModel/BaseRootModel.js +2 -1
  2. package/esm/RootModel/PreferencesConfig.d.ts +15 -0
  3. package/esm/RootModel/PreferencesConfig.js +15 -0
  4. package/esm/RootModel/createConfigModel.d.ts +8 -0
  5. package/esm/Session/BaseSession.d.ts +11 -26
  6. package/esm/Session/BaseSession.js +21 -60
  7. package/esm/Session/MultipleViews.d.ts +17 -3
  8. package/esm/Session/MultipleViews.js +45 -5
  9. package/esm/Session/Preferences.d.ts +2 -1
  10. package/esm/Session/Preferences.js +5 -0
  11. package/esm/Session/SessionTracks.d.ts +4 -1
  12. package/esm/Session/SessionTracks.js +8 -0
  13. package/esm/Session/TrackMenu.js +2 -2
  14. package/esm/Session/Tracks.d.ts +4 -5
  15. package/esm/Session/Tracks.js +115 -92
  16. package/esm/Session/developerMode.d.ts +7 -0
  17. package/esm/Session/developerMode.js +43 -0
  18. package/esm/Session/readdedTrackConf.d.ts +8 -0
  19. package/esm/Session/readdedTrackConf.js +25 -0
  20. package/esm/Session/temporaryAssemblyTracks.d.ts +4 -3
  21. package/esm/Session/temporaryAssemblyTracks.js +6 -7
  22. package/esm/controllerTracks.d.ts +3 -3
  23. package/esm/controllerTracks.js +17 -4
  24. package/esm/index.d.ts +2 -2
  25. package/esm/index.js +2 -2
  26. package/esm/loadPlugins.d.ts +1 -1
  27. package/esm/loadPlugins.js +6 -3
  28. package/esm/pruneUnbuildableNodes.js +5 -2
  29. package/esm/rpcWorker.js +5 -4
  30. package/esm/ui/AboutDialogContents.js +2 -1
  31. package/esm/ui/DescriptionPanel.d.ts +3 -0
  32. package/esm/ui/DescriptionPanel.js +24 -0
  33. package/esm/ui/DisplayDefaultsSection.d.ts +7 -20
  34. package/esm/ui/DisplayDefaultsSection.js +9 -47
  35. package/esm/ui/GeneralPreferencesTab.d.ts +14 -0
  36. package/esm/ui/GeneralPreferencesTab.js +27 -0
  37. package/esm/ui/PreferenceCheckbox.d.ts +6 -0
  38. package/esm/ui/PreferenceCheckbox.js +16 -0
  39. package/esm/ui/PreferencesDialog.d.ts +6 -27
  40. package/esm/ui/PreferencesDialog.js +38 -119
  41. package/esm/ui/ViewPreferencesTab.d.ts +12 -0
  42. package/esm/ui/ViewPreferencesTab.js +15 -0
  43. package/esm/ui/descriptionHtml.d.ts +26 -0
  44. package/esm/ui/descriptionHtml.js +66 -0
  45. package/esm/ui/displayDefaultChanges.d.ts +31 -0
  46. package/esm/ui/displayDefaultChanges.js +66 -0
  47. package/esm/ui/preferencesReset.d.ts +18 -0
  48. package/esm/ui/preferencesReset.js +64 -0
  49. package/esm/useEngineLifecycle.d.ts +7 -1
  50. package/esm/useEngineLifecycle.js +24 -2
  51. package/package.json +5 -4
  52. package/esm/scheduleDetachedDestroy.d.ts +0 -18
  53. package/esm/scheduleDetachedDestroy.js +0 -24
@@ -1,33 +1,20 @@
1
+ import type { DisplayDefaultsSession } from './displayDefaultChanges.ts';
1
2
  import type PluginManager from '@jbrowse/core/PluginManager';
2
- export interface DisplayDefaultsSession {
3
- getDisplayTypeDefaults: () => {
4
- displayType: string;
5
- slot: string;
6
- value: unknown;
7
- }[];
8
- setDisplayTypeDefault: (displayType: string, slot: string, value: unknown) => void;
9
- }
3
+ export type { DisplayDefaultsSession } from './displayDefaultChanges.ts';
10
4
  /**
11
5
  * The inventory of session-wide display-type defaults: every value the user has
12
6
  * pinned from a track menu, what it overrides, and a button to clear one.
13
7
  *
14
8
  * The pin is easy to set and, until this, hard to find again. A pinned default
15
9
  * shows on the track-selector badge of any **open** track it moves — but one
16
- * affecting nothing currently open, one promoted to a value equal to the slot's
17
- * base, and one that no longer resolves at all (a stale value the cascade
18
- * refuses, or a display type whose plugin is gone) appear nowhere. The only
19
- * place that listed them was the "Reset preferences to defaults" confirmation,
20
- * which is a destructive dialog to have to open in order to read something.
10
+ * affecting nothing currently open, and one that no longer resolves at all (a
11
+ * stale value the cascade refuses, or a display type whose plugin is gone)
12
+ * appear nowhere. The only place that listed them was the "Reset preferences to
13
+ * defaults" confirmation, which is a destructive dialog to have to open in
14
+ * order to read something.
21
15
  *
22
16
  * The empty state is deliberate rather than rendering nothing: it is the one
23
17
  * place the app says the capability exists to someone who has never used it.
24
- *
25
- * **The slot keeps its schema name**, while the display type gets its authored
26
- * one. A slot has no menu label to borrow — which row a setting is offered on is
27
- * a menu fact and declaring the slot promotable is a schema fact, and nothing
28
- * joins them (that split is why `promotableSlotsWithoutPin` has to walk a built
29
- * menu at all). A raw slot name is also what the config editor labels its fields
30
- * with, so the two surfaces agree.
31
18
  */
32
19
  declare const DisplayDefaultsSection: ({ session, pluginManager, }: {
33
20
  session: DisplayDefaultsSession;
@@ -1,65 +1,27 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { getConfigurationSchemaDefinition, isSlotDefinitionEntry, } from '@jbrowse/core/configuration';
3
2
  import { SettingsChangesTable } from '@jbrowse/core/ui';
4
3
  import { Typography } from '@mui/material';
5
4
  import { observer } from 'mobx-react';
6
- // The registered display types, by name.
7
- //
8
- // **Built rather than reached through `getDisplayType`, which THROWS** on a name
9
- // no plugin registers — an optional chain over it guards nothing. A default left
10
- // behind by a plugin that has since been uninstalled is exactly a row this
11
- // section has to render, so reaching for that getter would crash the whole
12
- // Preferences dialog for the one user whose pins most need clearing.
13
- function displayTypesByName(pluginManager) {
14
- return new Map(pluginManager.getDisplayElements().map(d => [d.name, d]));
15
- }
16
- // What a track sitting at the bottom of the cascade shows for this slot — the
17
- // value clearing the pin would restore. Read off the display type's schema
18
- // rather than off the session, which holds only what was promoted. Undefined
19
- // for a display type that is gone, whose row then reads "(default)" like any
20
- // other unknown.
21
- function promotedBaseOf(configSchema, slot) {
22
- const def = configSchema
23
- ? getConfigurationSchemaDefinition(configSchema)?.[slot]
24
- : undefined;
25
- return isSlotDefinitionEntry(def) ? def.promotedBase : undefined;
26
- }
5
+ import { displayDefaultChanges, resetDisplayDefault, } from './displayDefaultChanges.js';
27
6
  /**
28
7
  * The inventory of session-wide display-type defaults: every value the user has
29
8
  * pinned from a track menu, what it overrides, and a button to clear one.
30
9
  *
31
10
  * The pin is easy to set and, until this, hard to find again. A pinned default
32
11
  * shows on the track-selector badge of any **open** track it moves — but one
33
- * affecting nothing currently open, one promoted to a value equal to the slot's
34
- * base, and one that no longer resolves at all (a stale value the cascade
35
- * refuses, or a display type whose plugin is gone) appear nowhere. The only
36
- * place that listed them was the "Reset preferences to defaults" confirmation,
37
- * which is a destructive dialog to have to open in order to read something.
12
+ * affecting nothing currently open, and one that no longer resolves at all (a
13
+ * stale value the cascade refuses, or a display type whose plugin is gone)
14
+ * appear nowhere. The only place that listed them was the "Reset preferences to
15
+ * defaults" confirmation, which is a destructive dialog to have to open in
16
+ * order to read something.
38
17
  *
39
18
  * The empty state is deliberate rather than rendering nothing: it is the one
40
19
  * place the app says the capability exists to someone who has never used it.
41
- *
42
- * **The slot keeps its schema name**, while the display type gets its authored
43
- * one. A slot has no menu label to borrow — which row a setting is offered on is
44
- * a menu fact and declaring the slot promotable is a schema fact, and nothing
45
- * joins them (that split is why `promotableSlotsWithoutPin` has to walk a built
46
- * menu at all). A raw slot name is also what the config editor labels its fields
47
- * with, so the two surfaces agree.
48
20
  */
49
21
  const DisplayDefaultsSection = observer(function DisplayDefaultsSection({ session, pluginManager, }) {
50
- const byName = displayTypesByName(pluginManager);
51
- const changes = session
52
- .getDisplayTypeDefaults()
53
- .map(({ displayType, slot, value }) => ({
54
- // the address this section's own reset uses, so it depends on nothing the
55
- // session's preference-key layout does
56
- path: [displayType, slot],
57
- label: `${byName.get(displayType)?.displayName ?? displayType} › ${slot}`,
58
- from: promotedBaseOf(byName.get(displayType)?.configSchema, slot),
59
- to: value,
60
- }));
22
+ const changes = displayDefaultChanges(session, pluginManager);
61
23
  return changes.length ? (_jsxs(_Fragment, { children: [_jsx(Typography, { children: "These apply to every track of a display type that hasn't set its own value. Clear one to put those tracks back on the setting's default." }), _jsx(SettingsChangesTable, { changes: changes, onResetRow: change => {
62
- session.setDisplayTypeDefault(change.path[0], change.path[1], undefined);
63
- } })] })) : (_jsx(Typography, { children: "None set. The pin beside a setting in a track menu makes that value the default for every track of the same display type." }));
24
+ resetDisplayDefault(session, change);
25
+ } })] })) : (_jsx(Typography, { children: "None set. The pin beside a setting in a track menu applies that value to every open track of the same display type, and then offers to keep it as the default for the ones you open later." }));
64
26
  });
65
27
  export default DisplayDefaultsSection;
@@ -0,0 +1,14 @@
1
+ import type { ThemeMap } from '@jbrowse/core/ui';
2
+ import type { AnimationMode } from '@jbrowse/core/util';
3
+ export interface GeneralPreferencesSession {
4
+ allThemes: () => ThemeMap;
5
+ themeName?: string;
6
+ setThemeName: (arg: string) => void;
7
+ animationMode: AnimationMode;
8
+ numberGrouping: boolean;
9
+ setPreferenceOverride: (key: string, value: unknown) => void;
10
+ }
11
+ declare const GeneralPreferencesTab: ({ session, }: {
12
+ session: GeneralPreferencesSession;
13
+ }) => import("react").JSX.Element;
14
+ export default GeneralPreferencesTab;
@@ -0,0 +1,27 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
3
+ import { FormGroup, MenuItem, TextField } from '@mui/material';
4
+ import { observer } from 'mobx-react';
5
+ import PreferenceCheckbox from './PreferenceCheckbox.js';
6
+ const useStyles = makeStyles()({
7
+ field: {
8
+ marginTop: 16,
9
+ display: 'block',
10
+ },
11
+ });
12
+ const ANIMATION_MODES = [
13
+ { value: 'system', label: 'Follow system (reduced motion)' },
14
+ { value: 'enabled', label: 'Always on' },
15
+ { value: 'disabled', label: 'Off' },
16
+ ];
17
+ const GeneralPreferencesTab = observer(function GeneralPreferencesTab({ session, }) {
18
+ const { classes } = useStyles();
19
+ return (_jsxs(_Fragment, { children: [_jsx(TextField, { select: true, variant: "outlined", className: classes.field, label: "Theme", value: session.themeName, onChange: event => {
20
+ session.setThemeName(event.target.value);
21
+ }, children: Object.entries(session.allThemes()).map(([key, val]) => (_jsx(MenuItem, { value: key, children: val.name || '(Unknown name)' }, key))) }), _jsx(TextField, { select: true, variant: "outlined", className: classes.field, label: "Animations", value: session.animationMode, onChange: event => {
22
+ session.setPreferenceOverride('animationMode', event.target.value);
23
+ }, children: ANIMATION_MODES.map(opt => (_jsx(MenuItem, { value: opt.value, children: opt.label }, opt.value))) }), _jsx(FormGroup, { children: _jsx(PreferenceCheckbox, { checked: session.numberGrouping, label: "Thousand separators in numbers", help: "Shows chr1:1,234,567 rather than chr1:1234567. Takes effect after reloading the page.", onChange: checked => {
24
+ session.setPreferenceOverride('numberGrouping', checked);
25
+ } }) })] }));
26
+ });
27
+ export default GeneralPreferencesTab;
@@ -0,0 +1,6 @@
1
+ export default function PreferenceCheckbox({ checked, label, help, onChange, }: {
2
+ checked: boolean;
3
+ label: string;
4
+ help?: string;
5
+ onChange: (checked: boolean) => void;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { LabeledCheckbox } from '@jbrowse/core/ui';
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
4
+ import { Typography } from '@mui/material';
5
+ const useStyles = makeStyles()({
6
+ row: {
7
+ alignItems: 'flex-start',
8
+ marginTop: 8,
9
+ },
10
+ });
11
+ export default function PreferenceCheckbox({ checked, label, help, onChange, }) {
12
+ const { classes } = useStyles();
13
+ return (_jsx(LabeledCheckbox, { className: classes.row, checked: checked, onChange: checked => {
14
+ onChange(checked);
15
+ }, label: _jsxs(_Fragment, { children: [_jsx(Typography, { children: label }), help ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: help })) : null] }) }));
16
+ }
@@ -1,35 +1,14 @@
1
+ import type { DisplayDefaultsSession } from './DisplayDefaultsSection.tsx';
2
+ import type { GeneralPreferencesSession } from './GeneralPreferencesTab.tsx';
3
+ import type { ViewPreferencesSession } from './ViewPreferencesTab.tsx';
4
+ import type { ResettablePreferencesSession } from './preferencesReset.ts';
1
5
  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
6
  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;
7
+ export interface PreferencesDialogSession extends GeneralPreferencesSession, ViewPreferencesSession, DisplayDefaultsSession, ResettablePreferencesSession {
29
8
  }
30
9
  /**
31
10
  * Descriptor returned from the `Core-preferencesDialogPanels` extension point.
32
- * Each panel renders as its own labeled section in the dialog.
11
+ * Each panel renders as its own tab in the dialog, labeled by `name`.
33
12
  */
34
13
  export interface PreferencesPanelDescriptor {
35
14
  name: string;
@@ -1,140 +1,59 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
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';
3
+ import { Dialog } from '@jbrowse/core/ui';
5
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
6
- import { Button, DialogActions, DialogContent, Divider, FormGroup, MenuItem, TextField, Typography, } from '@mui/material';
5
+ import { Button, DialogActions, DialogContent, Tab, Tabs } from '@mui/material';
7
6
  import { observer } from 'mobx-react';
8
7
  import DisplayDefaultsSection from './DisplayDefaultsSection.js';
8
+ import GeneralPreferencesTab from './GeneralPreferencesTab.js';
9
9
  import PreferencesResetDialog from './PreferencesResetDialog.js';
10
+ import ViewPreferencesTab from './ViewPreferencesTab.js';
11
+ import { collectPreferenceChanges, resetAllPreferences, resetPreferenceChange, } from './preferencesReset.js';
10
12
  const useStyles = makeStyles()({
11
13
  container: {
12
14
  width: 800,
13
- },
14
- panelHeading: {
15
- marginTop: 16,
16
- },
17
- field: {
18
- marginTop: 16,
19
- display: 'block',
15
+ minHeight: 300,
20
16
  },
21
17
  });
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
18
  const PreferencesDialog = observer(function PreferencesDialog({ handleClose, session, pluginManager, }) {
114
19
  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
20
+ const [tab, setTab] = useState('general');
117
21
  const [resetDialogOpen, setResetDialogOpen] = useState(false);
118
- const extraPanels = pluginManager.evaluateExtensionPoint(
119
- /** #extensionPoint Core-preferencesDialogPanels | sync | Add panels to the preferences dialog */
22
+ const pluginPanels = pluginManager.evaluateExtensionPoint(
23
+ /** #extensionPoint Core-preferencesDialogPanels | sync | Add tabs to the preferences dialog */
120
24
  '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: () => {
25
+ const displayDefaultCount = session.getDisplayTypeDefaults().length;
26
+ const tabs = [
27
+ {
28
+ value: 'general',
29
+ label: 'General',
30
+ content: _jsx(GeneralPreferencesTab, { session: session }),
31
+ },
32
+ {
33
+ value: 'views',
34
+ label: 'Views',
35
+ content: _jsx(ViewPreferencesTab, { session: session }),
36
+ },
37
+ {
38
+ value: 'displayDefaults',
39
+ label: displayDefaultCount
40
+ ? `Display defaults (${displayDefaultCount})`
41
+ : 'Display defaults',
42
+ content: (_jsx(DisplayDefaultsSection, { session: session, pluginManager: pluginManager })),
43
+ },
44
+ ...pluginPanels.map(({ name, Component }) => ({
45
+ value: `plugin:${name}`,
46
+ label: name,
47
+ content: (_jsx(Suspense, { fallback: null, children: _jsx(Component, { session: session }) })),
48
+ })),
49
+ ];
50
+ return (_jsxs(Dialog, { title: "Preferences", open: true, onClose: handleClose, maxWidth: "xl", children: [_jsx(Tabs, { value: tab, variant: "scrollable", scrollButtons: "auto", onChange: (_, value) => {
51
+ setTab(value);
52
+ }, children: tabs.map(t => (_jsx(Tab, { value: t.value, label: t.label }, t.value))) }), _jsx(DialogContent, { className: classes.container, children: tabs.find(t => t.value === tab)?.content }), _jsxs(DialogActions, { children: [_jsx(Button, { variant: "contained", color: "secondary", onClick: () => {
134
53
  setResetDialogOpen(true);
135
54
  }, children: "Reset to defaults\u2026" }), _jsx(Button, { variant: "contained", onClick: () => {
136
55
  handleClose();
137
- }, children: "Close" })] }), resetDialogOpen ? (_jsx(PreferencesResetDialog, { changes: collectPreferenceChanges(session), onReset: () => {
56
+ }, children: "Close" })] }), resetDialogOpen ? (_jsx(PreferencesResetDialog, { changes: collectPreferenceChanges(session, pluginManager), onReset: () => {
138
57
  resetAllPreferences(session);
139
58
  }, onResetRow: change => {
140
59
  resetPreferenceChange(session, change);
@@ -0,0 +1,12 @@
1
+ export interface ViewPreferencesSession {
2
+ scrollZoom: boolean;
3
+ setScrollZoom: (flag: boolean) => void;
4
+ stickyViewHeaders: boolean;
5
+ setStickyViewHeaders: (sticky: boolean) => void;
6
+ effectiveUseWorkspaces: boolean;
7
+ setUseWorkspacesPreference: (useWorkspaces: boolean) => void;
8
+ }
9
+ declare const ViewPreferencesTab: ({ session, }: {
10
+ session: ViewPreferencesSession;
11
+ }) => import("react").JSX.Element;
12
+ export default ViewPreferencesTab;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SCROLL_ZOOM_HELP, SCROLL_ZOOM_LABEL, } from '@jbrowse/core/ui/scrollZoomLabels';
3
+ import { FormGroup } from '@mui/material';
4
+ import { observer } from 'mobx-react';
5
+ import PreferenceCheckbox from './PreferenceCheckbox.js';
6
+ const ViewPreferencesTab = observer(function ViewPreferencesTab({ session, }) {
7
+ return (_jsxs(FormGroup, { children: [_jsx(PreferenceCheckbox, { checked: session.scrollZoom, label: SCROLL_ZOOM_LABEL, help: SCROLL_ZOOM_HELP, onChange: checked => {
8
+ session.setScrollZoom(checked);
9
+ } }), _jsx(PreferenceCheckbox, { checked: session.stickyViewHeaders, label: "Keep view header visible", help: "The view header stays at the top of the page while its tracks scroll under it.", onChange: checked => {
10
+ session.setStickyViewHeaders(checked);
11
+ } }), _jsx(PreferenceCheckbox, { checked: session.effectiveUseWorkspaces, label: "Use workspaces", help: "Views open as tabs and tiles instead of stacking down the page.", onChange: checked => {
12
+ session.setUseWorkspacesPreference(checked);
13
+ } })] }));
14
+ });
15
+ export default ViewPreferencesTab;
@@ -0,0 +1,26 @@
1
+ import type { AboutConfig } from './util.ts';
2
+ /**
3
+ * The URL of a track's prose description page, as a UCSC track hub supplies it,
4
+ * and undefined for anything that is not an http(s) url — the value reaches
5
+ * `openLocation` and an anchor's href, so a `javascript:` or `file://` one does
6
+ * not become one here.
7
+ *
8
+ * Four spellings: `UCSCTrackHubConnection` spreads the trackDb stanza across
9
+ * `metadata`, the bulk converter behind genomes.jbrowse.org nests it under
10
+ * `metadata.ucsc` (~50,700 configs at permanent URLs), and the stanza key is
11
+ * `html` for a track and `htmlPath` for an assembly. Both producers write an
12
+ * escaped anchor rather than a bare url, so the href comes back through a parse
13
+ * rather than a regex.
14
+ */
15
+ export declare function getDescriptionHtmlUrl(config: AboutConfig): string | undefined;
16
+ /**
17
+ * A fetched description page as markup for `SanitizedHTML`: links and images
18
+ * resolved against the page they came from rather than the JBrowse origin, and
19
+ * `<form>` dropped, which the sanitizer keeps and which in a document off a
20
+ * third-party origin is a credential prompt wearing the app's own chrome.
21
+ *
22
+ * Parsing rather than string-rewriting is what makes taking `body` handle the
23
+ * pages that are whole documents in the same pass. The parse is inert — no
24
+ * scripts run, no images load — and the sanitizer still sees everything after.
25
+ */
26
+ export declare function rebaseDescriptionHtml(html: string, baseUrl: string): string;
@@ -0,0 +1,66 @@
1
+ import { readConfSlot } from '@jbrowse/core/configuration';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null;
4
+ }
5
+ function descriptionUrl(value) {
6
+ const href = new DOMParser()
7
+ .parseFromString(value, 'text/html')
8
+ .querySelector('a[href]')
9
+ ?.getAttribute('href');
10
+ const candidate = typeof href === 'string' ? href : value.trim();
11
+ return /^https?:\/\//i.test(candidate) ? candidate : undefined;
12
+ }
13
+ /**
14
+ * The URL of a track's prose description page, as a UCSC track hub supplies it,
15
+ * and undefined for anything that is not an http(s) url — the value reaches
16
+ * `openLocation` and an anchor's href, so a `javascript:` or `file://` one does
17
+ * not become one here.
18
+ *
19
+ * Four spellings: `UCSCTrackHubConnection` spreads the trackDb stanza across
20
+ * `metadata`, the bulk converter behind genomes.jbrowse.org nests it under
21
+ * `metadata.ucsc` (~50,700 configs at permanent URLs), and the stanza key is
22
+ * `html` for a track and `htmlPath` for an assembly. Both producers write an
23
+ * escaped anchor rather than a bare url, so the href comes back through a parse
24
+ * rather than a regex.
25
+ */
26
+ export function getDescriptionHtmlUrl(config) {
27
+ const metadata = readConfSlot(config, 'metadata');
28
+ return [metadata, isRecord(metadata) ? metadata.ucsc : undefined]
29
+ .flatMap(record => (isRecord(record) ? [record.html, record.htmlPath] : []))
30
+ .filter(candidate => typeof candidate === 'string')
31
+ .map(candidate => descriptionUrl(candidate))
32
+ .find(url => url !== undefined);
33
+ }
34
+ function absolute(value, baseUrl) {
35
+ try {
36
+ return new URL(value, baseUrl).href;
37
+ }
38
+ catch {
39
+ return value;
40
+ }
41
+ }
42
+ /**
43
+ * A fetched description page as markup for `SanitizedHTML`: links and images
44
+ * resolved against the page they came from rather than the JBrowse origin, and
45
+ * `<form>` dropped, which the sanitizer keeps and which in a document off a
46
+ * third-party origin is a credential prompt wearing the app's own chrome.
47
+ *
48
+ * Parsing rather than string-rewriting is what makes taking `body` handle the
49
+ * pages that are whole documents in the same pass. The parse is inert — no
50
+ * scripts run, no images load — and the sanitizer still sees everything after.
51
+ */
52
+ export function rebaseDescriptionHtml(html, baseUrl) {
53
+ const doc = new DOMParser().parseFromString(html, 'text/html');
54
+ for (const form of doc.querySelectorAll('form')) {
55
+ form.remove();
56
+ }
57
+ for (const el of doc.querySelectorAll('[href], [src]')) {
58
+ for (const attribute of ['href', 'src']) {
59
+ const value = el.getAttribute(attribute);
60
+ if (value) {
61
+ el.setAttribute(attribute, absolute(value, baseUrl));
62
+ }
63
+ }
64
+ }
65
+ return doc.body.innerHTML;
66
+ }
@@ -0,0 +1,31 @@
1
+ import type PluginManager from '@jbrowse/core/PluginManager';
2
+ import type { TrackConfigChange } from '@jbrowse/core/util';
3
+ export interface DisplayDefaultsSession {
4
+ getDisplayTypeDefaults: () => {
5
+ displayType: string;
6
+ slot: string;
7
+ value: unknown;
8
+ }[];
9
+ setDisplayTypeDefault: (displayType: string, slot: string, value: unknown) => void;
10
+ }
11
+ /**
12
+ * Every promoted display-type default as a settings row: what it overrides
13
+ * (the slot's `promotedBase`), the value, and a label naming the display type
14
+ * the way the user knows it. One builder for the two surfaces that list them,
15
+ * the Display defaults section and the "Reset to defaults" confirmation, which
16
+ * used to render the same default two ways.
17
+ *
18
+ * **The slot keeps its schema name**, while the display type gets its authored
19
+ * one. A slot has no menu label to borrow — which row a setting is offered on is
20
+ * a menu fact and declaring the slot promotable is a schema fact, and nothing
21
+ * joins them (that split is why `promotableSlotsWithoutPin` has to walk a built
22
+ * menu at all). A raw slot name is also what the config editor labels its fields
23
+ * with, so the two surfaces agree.
24
+ */
25
+ export declare function displayDefaultChanges(session: DisplayDefaultsSession, pluginManager: PluginManager): TrackConfigChange[];
26
+ /**
27
+ * Clear the promoted default a row built by {@link displayDefaultChanges}
28
+ * stands for. Returns false for any other row, so a table mixing both kinds
29
+ * can hand every reset here first.
30
+ */
31
+ export declare function resetDisplayDefault(session: DisplayDefaultsSession, change: TrackConfigChange): boolean;
@@ -0,0 +1,66 @@
1
+ import { getConfigurationSchemaDefinition, isSlotDefinitionEntry, } from '@jbrowse/core/configuration';
2
+ // Head of the path a promoted default's row carries, so `resetDisplayDefault`
3
+ // can tell it from a scalar preference row in the same table. Module-private:
4
+ // the one producer and the one consumer of the shape are both here, which is
5
+ // what lets the session know nothing about how the Preferences dialog
6
+ // addresses a row.
7
+ const DISPLAY_DEFAULT_PATH_HEAD = 'displayTypeDefaults';
8
+ // The registered display types, by name.
9
+ //
10
+ // **Built rather than reached through `getDisplayType`, which THROWS** on a name
11
+ // no plugin registers — an optional chain over it guards nothing. A default left
12
+ // behind by a plugin that has since been uninstalled is exactly a row this
13
+ // section has to render, so reaching for that getter would crash the whole
14
+ // Preferences dialog for the one user whose pins most need clearing.
15
+ function displayTypesByName(pluginManager) {
16
+ return new Map(pluginManager.getDisplayElements().map(d => [d.name, d]));
17
+ }
18
+ // What a track sitting at the bottom of the cascade shows for this slot — the
19
+ // value clearing the pin would restore. Read off the display type's schema
20
+ // rather than off the session, which holds only what was promoted. Undefined
21
+ // for a display type that is gone, whose row then reads "(default)" like any
22
+ // other unknown.
23
+ function promotedBaseOf(configSchema, slot) {
24
+ const def = configSchema
25
+ ? getConfigurationSchemaDefinition(configSchema)?.[slot]
26
+ : undefined;
27
+ return isSlotDefinitionEntry(def) ? def.promotedBase : undefined;
28
+ }
29
+ /**
30
+ * Every promoted display-type default as a settings row: what it overrides
31
+ * (the slot's `promotedBase`), the value, and a label naming the display type
32
+ * the way the user knows it. One builder for the two surfaces that list them,
33
+ * the Display defaults section and the "Reset to defaults" confirmation, which
34
+ * used to render the same default two ways.
35
+ *
36
+ * **The slot keeps its schema name**, while the display type gets its authored
37
+ * one. A slot has no menu label to borrow — which row a setting is offered on is
38
+ * a menu fact and declaring the slot promotable is a schema fact, and nothing
39
+ * joins them (that split is why `promotableSlotsWithoutPin` has to walk a built
40
+ * menu at all). A raw slot name is also what the config editor labels its fields
41
+ * with, so the two surfaces agree.
42
+ */
43
+ export function displayDefaultChanges(session, pluginManager) {
44
+ const byName = displayTypesByName(pluginManager);
45
+ return session
46
+ .getDisplayTypeDefaults()
47
+ .map(({ displayType, slot, value }) => ({
48
+ path: [DISPLAY_DEFAULT_PATH_HEAD, displayType, slot],
49
+ label: `${byName.get(displayType)?.displayName ?? displayType} › ${slot}`,
50
+ from: promotedBaseOf(byName.get(displayType)?.configSchema, slot),
51
+ to: value,
52
+ }));
53
+ }
54
+ /**
55
+ * Clear the promoted default a row built by {@link displayDefaultChanges}
56
+ * stands for. Returns false for any other row, so a table mixing both kinds
57
+ * can hand every reset here first.
58
+ */
59
+ export function resetDisplayDefault(session, change) {
60
+ const [head, displayType, slot] = change.path;
61
+ const isDisplayDefault = head === DISPLAY_DEFAULT_PATH_HEAD && !!displayType && !!slot;
62
+ if (isDisplayDefault) {
63
+ session.setDisplayTypeDefault(displayType, slot, undefined);
64
+ }
65
+ return isDisplayDefault;
66
+ }