@jbrowse/app-core 4.3.0 → 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/esm/AppSession/AppRootModel.d.ts +30 -0
- package/esm/AppSession/AppRootModel.js +1 -0
- package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
- package/esm/AppSession/AppSessionMixin.js +62 -0
- package/esm/AppSession/index.d.ts +2 -0
- package/esm/AppSession/index.js +1 -0
- package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
- package/esm/Assemblies/AssembliesMixin.js +149 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +19 -0
- package/esm/HistoryManagement/index.js +42 -5
- package/esm/JBrowseConfig/RootConfiguration.d.ts +171 -78
- package/esm/JBrowseConfig/RootConfiguration.js +85 -7
- package/esm/JBrowseConfig/index.d.ts +173 -84
- package/esm/JBrowseConfig/index.js +62 -2
- package/esm/JBrowseModel/index.d.ts +180 -95
- package/esm/JBrowseModel/index.js +122 -21
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +325 -0
- package/esm/JbApi/jbApi.js +748 -0
- package/esm/RootMenu/index.d.ts +69 -3
- package/esm/RootMenu/index.js +96 -61
- package/esm/SessionSpec/index.d.ts +6 -0
- package/esm/SessionSpec/index.js +3 -0
- package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
- package/esm/SessionSpec/lgvUrlInit.js +158 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
- package/esm/SessionSpec/loadSessionSpec.js +392 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
- package/esm/SessionSpec/types.d.ts +31 -0
- package/esm/SessionSpec/types.js +1 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
- package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
- package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
- package/esm/WorkspaceLayout/PanelView.js +99 -0
- package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
- package/esm/WorkspaceLayout/TabStrip.js +226 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
- package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
- package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
- package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
- package/esm/WorkspaceLayout/dropZone.js +113 -0
- package/esm/WorkspaceLayout/index.d.ts +8 -0
- package/esm/WorkspaceLayout/index.js +4 -0
- package/esm/WorkspaceLayout/model.d.ts +248 -0
- package/esm/WorkspaceLayout/model.js +562 -0
- package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
- package/esm/WorkspaceLayout/panelChrome.js +4 -0
- package/esm/WorkspaceLayout/spec.d.ts +88 -0
- package/esm/WorkspaceLayout/spec.js +226 -0
- package/esm/WorkspaceLayout/splitter.d.ts +25 -0
- package/esm/WorkspaceLayout/splitter.js +37 -0
- package/esm/WorkspaceLayout/tree.d.ts +158 -0
- package/esm/WorkspaceLayout/tree.js +379 -0
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
- package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
- package/esm/index.d.ts +18 -9
- package/esm/index.js +16 -9
- package/esm/menus.d.ts +55 -68
- package/esm/menus.js +117 -86
- package/esm/queryParams.d.ts +9 -0
- package/esm/queryParams.js +92 -0
- package/esm/sessionModelFactory.d.ts +18 -0
- package/esm/sessionModelFactory.js +1 -0
- package/esm/ui/App/App.d.ts +3 -14
- package/esm/ui/App/App.js +30 -17
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppReadyMarker.d.ts +43 -0
- package/esm/ui/App/AppReadyMarker.js +56 -0
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +23 -24
- package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
- package/esm/ui/App/ClassicViewsContainer.js +9 -8
- package/esm/ui/App/DialogQueue.d.ts +17 -1
- package/esm/ui/App/DialogQueue.js +30 -1
- package/esm/ui/App/Drawer.d.ts +1 -1
- package/esm/ui/App/Drawer.js +10 -39
- package/esm/ui/App/DrawerControls.d.ts +1 -5
- package/esm/ui/App/DrawerControls.js +1 -25
- package/esm/ui/App/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -10
- package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
- package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
- package/esm/ui/App/DrawerWidget.d.ts +1 -1
- package/esm/ui/App/DrawerWidget.js +9 -21
- package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
- package/esm/ui/App/JBrowseTabMenu.js +24 -5
- package/esm/ui/App/ViewButtons.d.ts +2 -4
- package/esm/ui/App/ViewButtons.js +15 -3
- package/esm/ui/App/ViewContainer.d.ts +3 -2
- package/esm/ui/App/ViewContainer.js +144 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +9 -2
- package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
- package/esm/ui/App/ViewErrorFallback.js +42 -0
- package/esm/ui/App/ViewHeader.d.ts +3 -4
- package/esm/ui/App/ViewHeader.js +28 -17
- package/esm/ui/App/ViewLauncher.d.ts +2 -8
- package/esm/ui/App/ViewLauncher.js +7 -5
- package/esm/ui/App/ViewMenu.d.ts +3 -5
- package/esm/ui/App/ViewMenu.js +87 -92
- package/esm/ui/App/ViewStack.d.ts +7 -0
- package/esm/ui/App/ViewStack.js +42 -0
- package/esm/ui/App/ViewWrapper.d.ts +15 -1
- package/esm/ui/App/ViewWrapper.js +26 -10
- package/esm/ui/App/ViewsContainer.d.ts +3 -8
- package/esm/ui/App/ViewsContainer.js +9 -6
- package/esm/ui/App/copyView.js +1 -1
- package/esm/ui/App/index.d.ts +2 -1
- package/esm/ui/App/index.js +3 -2
- package/esm/ui/App/lazyChunk.d.ts +13 -0
- package/esm/ui/App/lazyChunk.js +91 -0
- package/esm/ui/App/types.d.ts +12 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +34 -0
- package/esm/ui/App/useViewVisibility.js +87 -0
- package/esm/ui/App/viewTitle.d.ts +5 -0
- package/esm/ui/App/viewTitle.js +9 -0
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +12 -17
- package/esm/AppFocus/index.d.ts +0 -5
- package/esm/AppFocus/index.js +0 -12
- package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
- package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
- package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
- package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
- package/esm/DockviewLayout/index.d.ts +0 -35
- package/esm/DockviewLayout/index.js +0 -111
- package/esm/ui/App/DockviewContext.d.ts +0 -22
- package/esm/ui/App/DockviewContext.js +0 -29
- package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
- package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
- package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
- package/esm/ui/App/DrawerWidgetSelector.js +0 -47
- package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
- package/esm/ui/App/JBrowseViewPanel.js +0 -40
- package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
- package/esm/ui/App/JBrowseViewTab.js +0 -97
- package/esm/ui/App/ModalWidget.d.ts +0 -6
- package/esm/ui/App/ModalWidget.js +0 -48
- package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
- package/esm/ui/App/TiledViewsContainer.js +0 -366
- package/esm/ui/App/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
|
|
3
4
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
5
|
import CloseIcon from '@mui/icons-material/Close';
|
|
5
6
|
import EditIcon from '@mui/icons-material/Edit';
|
|
6
7
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
7
8
|
const useStyles = makeStyles()({
|
|
9
|
+
// hidden until the tab is hovered or focused, by a rule in `TabStrip` keyed
|
|
10
|
+
// on the `jbrowse-tab-menu` class — hence the plain class name beside the
|
|
11
|
+
// generated one. `visibility` rather than `display`, so the tab does not
|
|
12
|
+
// change width under the pointer as it arrives.
|
|
8
13
|
tabIcons: {
|
|
9
14
|
display: 'flex',
|
|
10
15
|
alignItems: 'center',
|
|
11
16
|
visibility: 'hidden',
|
|
12
17
|
},
|
|
13
|
-
|
|
18
|
+
// the pointer is over the menu, not the tab, for as long as the menu is open,
|
|
19
|
+
// so the hover rule stops applying the moment it opens — leaving the popover
|
|
20
|
+
// anchored to an invisible button
|
|
21
|
+
open: {
|
|
14
22
|
visibility: 'visible',
|
|
15
23
|
},
|
|
16
24
|
tabIcon: {
|
|
@@ -22,9 +30,20 @@ const useStyles = makeStyles()({
|
|
|
22
30
|
fontSize: 14,
|
|
23
31
|
},
|
|
24
32
|
});
|
|
25
|
-
export default function JBrowseTabMenu({
|
|
26
|
-
const { classes } = useStyles();
|
|
27
|
-
|
|
33
|
+
export default function JBrowseTabMenu({ onRename, onClose, }) {
|
|
34
|
+
const { classes, cx } = useStyles();
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
return (_jsx("div", { className: cx('jbrowse-tab-menu', classes.tabIcons, open && classes.open),
|
|
37
|
+
// The tab strip starts a tab drag on pointerdown and takes POINTER
|
|
38
|
+
// CAPTURE for it, and a captured pointer retargets the compatibility
|
|
39
|
+
// mouse events with it — so `click` is delivered to the tab rather than
|
|
40
|
+
// to this button, and the menu silently never opens. Stopping the
|
|
41
|
+
// pointerdown here means a press that starts on the menu is not a drag
|
|
42
|
+
// and never captures, which is the same reason the rename input in
|
|
43
|
+
// `WorkspaceTab` stops it.
|
|
44
|
+
onPointerDown: event => {
|
|
45
|
+
event.stopPropagation();
|
|
46
|
+
}, children: _jsx(CascadingMenuButton, { menuItems: [
|
|
28
47
|
{
|
|
29
48
|
label: 'Rename tab',
|
|
30
49
|
icon: EditIcon,
|
|
@@ -35,5 +54,5 @@ export default function JBrowseTabMenu({ isHovered, onRename, onClose, }) {
|
|
|
35
54
|
icon: CloseIcon,
|
|
36
55
|
onClick: onClose,
|
|
37
56
|
},
|
|
38
|
-
], size: "small", className: classes.tabIcon, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
|
|
57
|
+
], size: "small", className: classes.tabIcon, setOpen: setOpen, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
|
|
39
58
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
|
|
2
|
-
declare const ViewButtons: ({ view,
|
|
2
|
+
declare const ViewButtons: ({ view, }: {
|
|
3
3
|
view: IBaseViewModel;
|
|
4
|
-
|
|
5
|
-
onMinimize: () => void;
|
|
6
|
-
}) => import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
}) => import("react").JSX.Element;
|
|
7
5
|
export default ViewButtons;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getSession } from '@jbrowse/core/util';
|
|
2
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
3
4
|
import AddIcon from '@mui/icons-material/Add';
|
|
4
5
|
import CloseIcon from '@mui/icons-material/Close';
|
|
@@ -10,8 +11,19 @@ const useStyles = makeStyles()(theme => ({
|
|
|
10
11
|
color: theme.palette.secondary.contrastText,
|
|
11
12
|
},
|
|
12
13
|
}));
|
|
13
|
-
|
|
14
|
+
// Acts on the view directly rather than taking onClose/onMinimize callbacks.
|
|
15
|
+
// Those were written inline by ViewContainer, three components up, so they were
|
|
16
|
+
// fresh on every one of its renders and mobx-react's memo could never hold for
|
|
17
|
+
// this subtree. A resize of the view body re-rendered these MUI buttons along
|
|
18
|
+
// with the header and the menu. There was nothing for the caller to decide
|
|
19
|
+
// anyway: both handlers are the same two lines wherever they are written.
|
|
20
|
+
const ViewButtons = observer(function ViewButtons({ view, }) {
|
|
14
21
|
const { classes } = useStyles();
|
|
15
|
-
|
|
22
|
+
const session = getSession(view);
|
|
23
|
+
return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "aria-label": view.minimized ? 'Restore view' : 'Minimize view', "data-testid": "minimize_view", onClick: () => {
|
|
24
|
+
view.setMinimized(!view.minimized);
|
|
25
|
+
}, children: view.minimized ? (_jsx(AddIcon, { className: classes.icon, fontSize: "small" })) : (_jsx(MinimizeIcon, { className: classes.icon, fontSize: "small" })) }), _jsx(IconButton, { "aria-label": "Close view", "data-testid": "close_view", onClick: () => {
|
|
26
|
+
session.removeView(view);
|
|
27
|
+
}, children: _jsx(CloseIcon, { className: classes.icon, fontSize: "small" }) })] }));
|
|
16
28
|
});
|
|
17
29
|
export default ViewButtons;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
|
|
2
|
-
declare const ViewContainer: ({ view, session, }: {
|
|
2
|
+
declare const ViewContainer: ({ view, session, scrollOnMount, }: {
|
|
3
3
|
view: AbstractViewModel;
|
|
4
4
|
session: SessionWithFocusedViewAndDrawerWidgets;
|
|
5
|
-
|
|
5
|
+
scrollOnMount?: boolean;
|
|
6
|
+
}) => import("react").JSX.Element;
|
|
6
7
|
export default ViewContainer;
|
|
@@ -1,16 +1,94 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
-
import { useWidthSetter } from '@jbrowse/core/util';
|
|
3
|
+
import { useFocusOnInteraction, useWidthSetter } from '@jbrowse/core/util/hooks';
|
|
4
4
|
import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
-
import {
|
|
5
|
+
import { isAlive } from '@jbrowse/mobx-state-tree';
|
|
6
|
+
import { Paper } from '@mui/material';
|
|
6
7
|
import { observer } from 'mobx-react';
|
|
7
|
-
import ViewHeader from
|
|
8
|
-
import ViewWrapper from
|
|
8
|
+
import ViewHeader from './ViewHeader.js';
|
|
9
|
+
import ViewWrapper from './ViewWrapper.js';
|
|
10
|
+
import { useViewVisibility } from './useViewVisibility.js';
|
|
11
|
+
import { viewTitle } from './viewTitle.js';
|
|
12
|
+
// Intended to keep views mounted within ~1.5 viewport-heights of the visible
|
|
13
|
+
// band. **It does nothing, and making it work was measured as a regression.**
|
|
14
|
+
//
|
|
15
|
+
// An observer clips the target against every scrolling ancestor before applying
|
|
16
|
+
// the margin, which expands only the root's box — and both view containers are
|
|
17
|
+
// `overflow-y: auto`, so through one of them `150% 0px` qualifies exactly the
|
|
18
|
+
// views a `0px` margin would. Rooting the observer at the scroll port instead
|
|
19
|
+
// (`scrollPortOf`) does restore the band, and it came out a wash on scroll cost
|
|
20
|
+
// while holding 9-13 live GPU canvases instead of 6. The mount band trades
|
|
21
|
+
// pipeline rebuilds for live contexts, and with the ceiling at 16 live WebGL2
|
|
22
|
+
// contexts the contexts are the tighter constraint.
|
|
23
|
+
// See agent-docs/reference/GPU_CONTEXT_BUDGET.md.
|
|
24
|
+
//
|
|
25
|
+
// So don't tune this number expecting an effect, and don't make it live without
|
|
26
|
+
// first cutting contexts per display.
|
|
27
|
+
const VIEW_VISIBILITY_ROOT_MARGIN = '150% 0px';
|
|
28
|
+
// Scroll-space reserved for a view that has never been measured yet, so a fresh
|
|
29
|
+
// load spreads views down the page instead of stacking them all in the viewport
|
|
30
|
+
// (which would mark them all visible and defeat the lazy mount).
|
|
31
|
+
const ESTIMATED_VIEW_HEIGHT = 400;
|
|
32
|
+
// CALL THIS ONLY WHERE THE ESTIMATE IS ACTUALLY USED, i.e. under `reserveSpace`.
|
|
33
|
+
// A view's `height` is a computed over its track heights (LinearGenomeView's
|
|
34
|
+
// sums them), so reading it makes the caller an observer of every track resize
|
|
35
|
+
// and every display that grows with its data. Called unconditionally during
|
|
36
|
+
// render, that re-rendered the whole view chrome on each of those, to produce
|
|
37
|
+
// a placeholder height that only a hidden view ever shows. Measured: 2 chrome
|
|
38
|
+
// renders per height change became 0.
|
|
39
|
+
function viewHeight(view) {
|
|
40
|
+
return 'height' in view && typeof view.height === 'number'
|
|
41
|
+
? view.height
|
|
42
|
+
: ESTIMATED_VIEW_HEIGHT;
|
|
43
|
+
}
|
|
44
|
+
// Every view that waits on something before it can mount content models that
|
|
45
|
+
// wait as `showLoading`. Views without the getter are mounted content the moment
|
|
46
|
+
// they render — which is the trap, because a view that waits and *doesn't*
|
|
47
|
+
// declare it is indistinguishable from one that never waits, and reports ready
|
|
48
|
+
// over a body that is plainly working. Spreadsheet and SV inspector were exactly
|
|
49
|
+
// that until they got the getter, and a spreadsheet mounts no displays, so no
|
|
50
|
+
// display-level wait covered for them either.
|
|
51
|
+
//
|
|
52
|
+
// LGV, dotplot, synteny, circular and breakpoint-split paint a spinner in place
|
|
53
|
+
// of their whole body meanwhile. Spreadsheet is the exception and keeps its
|
|
54
|
+
// import wizard mounted with a spinner above it, since the chosen file, type and
|
|
55
|
+
// assembly are worth more on screen than a bare loading screen — the phase is
|
|
56
|
+
// about the model, not about which component is mounted.
|
|
57
|
+
//
|
|
58
|
+
// Two values, not one per render branch: an import form is finished content, not
|
|
59
|
+
// a pending state, so it reports ready like anything else.
|
|
60
|
+
function viewPhase(view) {
|
|
61
|
+
return 'showLoading' in view && view.showLoading === true
|
|
62
|
+
? 'loading'
|
|
63
|
+
: 'ready';
|
|
64
|
+
}
|
|
9
65
|
const useStyles = makeStyles()(theme => ({
|
|
10
66
|
viewContainer: {
|
|
11
67
|
margin: theme.spacing(0.5),
|
|
12
68
|
padding: `0 ${theme.spacing(1)} ${theme.spacing(1)}`,
|
|
13
69
|
overflow: 'clip',
|
|
70
|
+
// xref https://stackoverflow.com/questions/43909940/why-does-overflowhidden-prevent-positionsticky-from-working
|
|
71
|
+
// note that contain:paint also seems to work
|
|
72
|
+
// `:focus-visible`, never `:focus`. The container carries tabIndex={0}, and
|
|
73
|
+
// a click on any non-focusable descendant of a tabbable node focuses that
|
|
74
|
+
// node — so on `:focus` this ring would appear around the whole view on
|
|
75
|
+
// essentially every click, which is a visible regression for every existing
|
|
76
|
+
// mouse user. `:focus-visible`'s heuristic excludes exactly that case.
|
|
77
|
+
//
|
|
78
|
+
// Inset (`outlineOffset: -2`) because `overflow: clip` above and the
|
|
79
|
+
// scroll port this sits in would clip a ring drawn outside the border box.
|
|
80
|
+
//
|
|
81
|
+
// `secondary.contrastText`, which is the same token the header's icons take,
|
|
82
|
+
// because inset is what decides the color: the ring lands entirely on this
|
|
83
|
+
// element's own secondary-colored band, so it has to contrast with THAT and
|
|
84
|
+
// not with the page. `primary.main` was the reflex and is midnight (#0D233F)
|
|
85
|
+
// against grape (#721E63) — drawn, measured on screen, and all but invisible.
|
|
86
|
+
// contrastText is the one token guaranteed legible on this background
|
|
87
|
+
// whatever an embedder's theme sets.
|
|
88
|
+
'&:focus-visible': {
|
|
89
|
+
outline: `2px solid ${theme.palette.secondary.contrastText}`,
|
|
90
|
+
outlineOffset: -2,
|
|
91
|
+
},
|
|
14
92
|
},
|
|
15
93
|
focusedView: {
|
|
16
94
|
background: theme.palette.secondary.main,
|
|
@@ -19,31 +97,73 @@ const useStyles = makeStyles()(theme => ({
|
|
|
19
97
|
background: theme.palette.secondary.dark,
|
|
20
98
|
},
|
|
21
99
|
}));
|
|
22
|
-
const ViewContainer = observer(function ViewContainer({ view, session, }) {
|
|
23
|
-
const
|
|
24
|
-
const ref = useWidthSetter(view, theme.spacing(1));
|
|
100
|
+
const ViewContainer = observer(function ViewContainer({ view, session, scrollOnMount, }) {
|
|
101
|
+
const ref = useWidthSetter(view);
|
|
25
102
|
const { classes } = useStyles();
|
|
103
|
+
const { ref: bodyRef, visible, measuredHeight, } = useViewVisibility(VIEW_VISIBILITY_ROOT_MARGIN);
|
|
104
|
+
useFocusOnInteraction(ref, () => {
|
|
105
|
+
session.setFocusedViewId(view.id);
|
|
106
|
+
});
|
|
107
|
+
// A minimized view renders no body, so it neither mounts the (GPU-heavy) view
|
|
108
|
+
// component nor reserves placeholder scroll-space; a spacer would otherwise
|
|
109
|
+
// re-expand the collapsed view to its pre-minimize height as soon as it
|
|
110
|
+
// scrolled out of the viewport.
|
|
111
|
+
const { minimized } = view;
|
|
112
|
+
const showBody = visible && !minimized;
|
|
113
|
+
const reserveSpace = !visible && !minimized;
|
|
114
|
+
// Tell the view whether its body is in the DOM. A display's phase counts its
|
|
115
|
+
// first paint as pending work, and a body that was never mounted has no
|
|
116
|
+
// canvas to paint — so without this an off-screen view holds the whole app's
|
|
117
|
+
// readiness marker at `loading` forever. See `BaseViewModel.bodyMounted`.
|
|
26
118
|
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
session.setFocusedViewId(view.id);
|
|
30
|
-
}
|
|
119
|
+
if (isAlive(view)) {
|
|
120
|
+
view.setBodyMounted(showBody);
|
|
31
121
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
122
|
+
}, [view, showBody]);
|
|
123
|
+
// ...and tell it when this container goes away entirely, which is the case
|
|
124
|
+
// the effect above cannot see: React unmounts without running it again, so a
|
|
125
|
+
// view whose container left the DOM went on claiming a mounted body. The
|
|
126
|
+
// workspace does that on every tab switch — only the tab being shown is
|
|
127
|
+
// mounted (`WorkspaceLayout/CLAUDE.md`) — and the view it switched away from
|
|
128
|
+
// parked the app's readiness marker at `loading` for the rest of the session.
|
|
129
|
+
//
|
|
130
|
+
// Its own effect, depending on the view alone, so it fires on an actual
|
|
131
|
+
// unmount and not between the two writes of a `showBody` change: those writes
|
|
132
|
+
// are MST actions, so a false the container did not mean would reach every
|
|
133
|
+
// display's phase before the true landed.
|
|
134
|
+
useEffect(() => () => {
|
|
135
|
+
if (isAlive(view)) {
|
|
136
|
+
view.setBodyMounted(false);
|
|
137
|
+
}
|
|
138
|
+
}, [view]);
|
|
139
|
+
// Only this container tracks focus now, not its header: with the arrow icon
|
|
140
|
+
// in its own observer (ViewFocusIndicator) the header's props are unchanged
|
|
141
|
+
// for every view but the two the focus moved between, so mobx-react's memo
|
|
142
|
+
// holds and one click re-renders two headers rather than all of them.
|
|
39
143
|
const backgroundColorClassName = session.focusedViewId === view.id
|
|
40
144
|
? classes.focusedView
|
|
41
145
|
: classes.unfocusedView;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
146
|
+
// Same title `ViewContainerTitle` shows, recomputed rather than shared through
|
|
147
|
+
// a ref or aria-labelledby: the visible one lives inside an EditableTypography
|
|
148
|
+
// whose rendered node is an <input> while it is being edited, and a label
|
|
149
|
+
// pointing at an input reads back as the input, not as the name. Both of its
|
|
150
|
+
// inputs (`displayName`, `assemblyNames`) are cold — a rename and a change of
|
|
151
|
+
// displayed region — so this adds no per-frame read.
|
|
152
|
+
const { assemblyManager } = session;
|
|
153
|
+
return (_jsxs(Paper, { ref: ref, elevation: 12, className: cx(classes.viewContainer, backgroundColorClassName),
|
|
154
|
+
// The view's one tab stop, and the only thing that makes the ctrl/cmd +
|
|
155
|
+
// arrow shortcuts reachable without a mouse: they are gated on
|
|
156
|
+
// `session.focusedViewId`, which `useFocusOnInteraction` now assigns from
|
|
157
|
+
// `focusin`. It goes on the outermost node so there is exactly one stop
|
|
158
|
+
// per view ahead of the header's own buttons, and so every view type gets
|
|
159
|
+
// it rather than each one growing its own.
|
|
160
|
+
tabIndex: 0,
|
|
161
|
+
// A tab stop with no role and no name announces as nothing. `region` is
|
|
162
|
+
// the conservative reading — it is what the session's view stack is, a set
|
|
163
|
+
// of labelled panels — and a named region shows up in a screen reader's
|
|
164
|
+
// landmark list, so "jump to the second view" stops being a Tab count.
|
|
165
|
+
role: "region", "aria-label": viewTitle(view, r => assemblyManager.getDisplayName(r)), "data-testid": `view-container-${view.id}`, "data-view-phase": viewPhase(view), children: [_jsx(ViewHeader, { view: view, className: backgroundColorClassName, scrollOnMount: scrollOnMount }), _jsx(Paper, { elevation: 0, children: _jsx("div", { ref: bodyRef, style: reserveSpace
|
|
166
|
+
? { height: measuredHeight ?? viewHeight(view) }
|
|
167
|
+
: undefined, children: showBody ? _jsx(ViewWrapper, { view: view, session: session }) : null }) })] }));
|
|
48
168
|
});
|
|
49
169
|
export default ViewContainer;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes';
|
|
2
2
|
declare const ViewContainerTitle: ({ view, }: {
|
|
3
3
|
view: IBaseViewModel;
|
|
4
|
-
}) => import("react
|
|
4
|
+
}) => import("react").JSX.Element;
|
|
5
5
|
export default ViewContainerTitle;
|
|
@@ -4,10 +4,17 @@ import { getSession } from '@jbrowse/core/util';
|
|
|
4
4
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
5
|
import { Tooltip } from '@mui/material';
|
|
6
6
|
import { observer } from 'mobx-react';
|
|
7
|
+
import { viewTitle } from './viewTitle.js';
|
|
7
8
|
const useStyles = makeStyles()(theme => ({
|
|
8
9
|
input: {
|
|
9
10
|
paddingBottom: 0,
|
|
10
11
|
paddingTop: 2,
|
|
12
|
+
// long view titles (e.g. a read-vs-ref panel named after a long PacBio
|
|
13
|
+
// QNAME) truncate with an ellipsis instead of overflowing the header
|
|
14
|
+
maxWidth: 300,
|
|
15
|
+
textOverflow: 'ellipsis',
|
|
16
|
+
whiteSpace: 'nowrap',
|
|
17
|
+
overflow: 'hidden',
|
|
11
18
|
},
|
|
12
19
|
inputBase: {
|
|
13
20
|
color: theme.palette.secondary.contrastText,
|
|
@@ -25,8 +32,8 @@ const useStyles = makeStyles()(theme => ({
|
|
|
25
32
|
const ViewContainerTitle = observer(function ViewContainerTitle({ view, }) {
|
|
26
33
|
const { classes } = useStyles();
|
|
27
34
|
const { assemblyManager } = getSession(view);
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
const title = viewTitle(view, r => assemblyManager.getDisplayName(r));
|
|
36
|
+
return (_jsx(Tooltip, { title: `${title} (click to rename)`, arrow: true, children: _jsx(EditableTypography, { value: title, setValue: val => {
|
|
30
37
|
view.setDisplayName(val);
|
|
31
38
|
}, variant: "body2", classes: {
|
|
32
39
|
input: classes.input,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
|
|
2
|
+
/**
|
|
3
|
+
* What a view that threw during render is replaced by: which view died, the
|
|
4
|
+
* error itself, and the two ways out that are not reloading the application.
|
|
5
|
+
*
|
|
6
|
+
* Retry clears the boundary and remounts the view component — worth offering
|
|
7
|
+
* because a good share of these are transient (a lazy chunk that failed to
|
|
8
|
+
* fetch, a GPU context lost). Close goes through the session's `removeView`,
|
|
9
|
+
* which detaches before it destroys; nothing here may destroy the node itself
|
|
10
|
+
* (ADR-069).
|
|
11
|
+
*/
|
|
12
|
+
declare const ViewErrorFallback: ({ view, session, error, onRetry, }: {
|
|
13
|
+
view: AbstractViewModel;
|
|
14
|
+
session: SessionWithFocusedViewAndDrawerWidgets;
|
|
15
|
+
error: unknown;
|
|
16
|
+
onRetry: () => void;
|
|
17
|
+
}) => import("react").JSX.Element;
|
|
18
|
+
export default ViewErrorFallback;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ErrorBanner } from '@jbrowse/core/ui';
|
|
3
|
+
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
|
+
import { Button } from '@mui/material';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
import { viewTitle } from './viewTitle.js';
|
|
7
|
+
const useStyles = makeStyles()(theme => ({
|
|
8
|
+
which: {
|
|
9
|
+
padding: `${theme.spacing(1)} ${theme.spacing(1)} 0`,
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
// The fallback must not be able to throw. What crashes a view is often one of
|
|
13
|
+
// its own getters, and `viewTitle` reads `assemblyNames`, which is one — so
|
|
14
|
+
// naming the view is exactly where a fallback can rethrow, and a rethrow here
|
|
15
|
+
// goes to the app boundary and puts up FatalErrorDialog, i.e. the whole-page
|
|
16
|
+
// failure this boundary exists to avoid. `displayName` and `type` are plain
|
|
17
|
+
// properties and cannot.
|
|
18
|
+
function safeViewTitle(view, session) {
|
|
19
|
+
try {
|
|
20
|
+
return viewTitle(view, r => session.assemblyManager.getDisplayName(r));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return view.displayName ?? 'Untitled view';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What a view that threw during render is replaced by: which view died, the
|
|
28
|
+
* error itself, and the two ways out that are not reloading the application.
|
|
29
|
+
*
|
|
30
|
+
* Retry clears the boundary and remounts the view component — worth offering
|
|
31
|
+
* because a good share of these are transient (a lazy chunk that failed to
|
|
32
|
+
* fetch, a GPU context lost). Close goes through the session's `removeView`,
|
|
33
|
+
* which detaches before it destroys; nothing here may destroy the node itself
|
|
34
|
+
* (ADR-069).
|
|
35
|
+
*/
|
|
36
|
+
const ViewErrorFallback = observer(function ViewErrorFallback({ view, session, error, onRetry, }) {
|
|
37
|
+
const { classes } = useStyles();
|
|
38
|
+
return (_jsxs("div", { children: [_jsx("div", { className: classes.which, children: `The ${view.type} "${safeViewTitle(view, session)}" could not be displayed` }), _jsx(ErrorBanner, { error: error, onReset: onRetry, extraAction: _jsx(Button, { "data-testid": "close_crashed_view", onClick: () => {
|
|
39
|
+
session.removeView(view);
|
|
40
|
+
}, children: "Close view" }) })] }));
|
|
41
|
+
});
|
|
42
|
+
export default ViewErrorFallback;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
|
|
2
|
-
declare const ViewHeader: ({ view,
|
|
2
|
+
declare const ViewHeader: ({ view, className, scrollOnMount, }: {
|
|
3
3
|
view: IBaseViewModel;
|
|
4
|
-
onClose: () => void;
|
|
5
|
-
onMinimize: () => void;
|
|
6
4
|
className?: string;
|
|
7
|
-
|
|
5
|
+
scrollOnMount?: boolean;
|
|
6
|
+
}) => import("react").JSX.Element;
|
|
8
7
|
export default ViewHeader;
|
package/esm/ui/App/ViewHeader.js
CHANGED
|
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { VIEW_HEADER_HEIGHT } from '@jbrowse/core/ui';
|
|
4
4
|
import { getSession } from '@jbrowse/core/util';
|
|
5
|
+
import { VIEW_HEADER_HEIGHT_VAR, useChromeHeightVar, } from '@jbrowse/core/util/hooks';
|
|
5
6
|
import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
|
|
6
|
-
import { isSessionWithMultipleViews } from '@jbrowse/product-core';
|
|
7
7
|
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
|
8
8
|
import { observer } from 'mobx-react';
|
|
9
|
-
import ViewButtons from
|
|
10
|
-
import ViewContainerTitle from
|
|
11
|
-
import ViewMenu from
|
|
9
|
+
import ViewButtons from './ViewButtons.js';
|
|
10
|
+
import ViewContainerTitle from './ViewContainerTitle.js';
|
|
11
|
+
import ViewMenu from './ViewMenu.js';
|
|
12
12
|
const useStyles = makeStyles()(theme => ({
|
|
13
13
|
icon: {
|
|
14
14
|
color: theme.palette.secondary.contrastText,
|
|
@@ -18,7 +18,11 @@ const useStyles = makeStyles()(theme => ({
|
|
|
18
18
|
},
|
|
19
19
|
viewHeader: {
|
|
20
20
|
display: 'flex',
|
|
21
|
-
height:
|
|
21
|
+
// a minimum, not a height: the constant exists so the sticky boxes below
|
|
22
|
+
// can clear this one, and pinning the box to it clipped the title row at a
|
|
23
|
+
// larger root font size. `useChromeHeightVar` publishes what it actually
|
|
24
|
+
// measures, so growing here moves them rather than overlapping them
|
|
25
|
+
minHeight: VIEW_HEADER_HEIGHT,
|
|
22
26
|
top: 0,
|
|
23
27
|
zIndex: 900,
|
|
24
28
|
},
|
|
@@ -27,22 +31,29 @@ const useStyles = makeStyles()(theme => ({
|
|
|
27
31
|
alignItems: 'center',
|
|
28
32
|
},
|
|
29
33
|
}));
|
|
30
|
-
|
|
34
|
+
// The one thing in the header that depends on which view has focus, kept in its
|
|
35
|
+
// own observer so the header does not. Read from ViewHeader itself,
|
|
36
|
+
// `focusedViewId` made a single click re-render every view's header and the MUI
|
|
37
|
+
// subtree under it (a Tooltip, three IconButtons and the menu), because an
|
|
38
|
+
// observer re-renders on its own observable change whatever its props say. Here
|
|
39
|
+
// a click re-renders N nodes that mostly render nothing.
|
|
40
|
+
const ViewFocusIndicator = observer(function ViewFocusIndicator({ view, className, }) {
|
|
41
|
+
return getSession(view).focusedViewId === view.id ? (_jsx(KeyboardArrowRightIcon, { className: className, fontSize: "small" })) : null;
|
|
42
|
+
});
|
|
43
|
+
const ViewHeader = observer(function ViewHeader({ view, className, scrollOnMount, }) {
|
|
31
44
|
const { classes } = useStyles();
|
|
32
45
|
const scrollRef = useRef(null);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
useChromeHeightVar(scrollRef, VIEW_HEADER_HEIGHT_VAR);
|
|
47
|
+
const stickyViewHeaders = getSession(view).stickyViewHeaders === true;
|
|
48
|
+
// Scroll a newly-added view into view on mount. Gated on scrollOnMount so a
|
|
49
|
+
// cold load / session restore with several views doesn't have every header
|
|
50
|
+
// race to scrollIntoView (last one wins, landing on the bottom view);
|
|
51
|
+
// ViewStack only sets it for views added after the initial render.
|
|
39
52
|
useEffect(() => {
|
|
40
|
-
if (
|
|
41
|
-
!navigator.webdriver &&
|
|
42
|
-
window.self === window.top) {
|
|
53
|
+
if (scrollOnMount && !navigator.webdriver && window.self === window.top) {
|
|
43
54
|
scrollRef.current?.scrollIntoView({ block: 'center' });
|
|
44
55
|
}
|
|
45
|
-
}, []);
|
|
46
|
-
return (_jsxs("div", { ref: scrollRef, className: cx(classes.viewHeader, className), style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(ViewMenu, { model: view,
|
|
56
|
+
}, [scrollOnMount]);
|
|
57
|
+
return (_jsxs("div", { ref: scrollRef, className: cx(classes.viewHeader, className), style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(ViewMenu, { model: view, className: classes.icon }), _jsx("div", { className: classes.grow }), _jsxs("div", { className: classes.viewTitle, children: [_jsx(ViewFocusIndicator, { view: view, className: classes.icon }), _jsx(ViewContainerTitle, { view: view })] }), _jsx("div", { className: classes.grow }), _jsx(ViewButtons, { view: view })] }));
|
|
47
58
|
});
|
|
48
59
|
export default ViewHeader;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
|
|
2
1
|
import type { SessionWithDrawerWidgets } from '@jbrowse/core/util';
|
|
3
|
-
type AppSession = SessionWithDrawerWidgets & {
|
|
4
|
-
snackbarMessages: SnackbarMessage[];
|
|
5
|
-
renameCurrentSession: (arg: string) => void;
|
|
6
|
-
popSnackbarMessage: () => unknown;
|
|
7
|
-
};
|
|
8
2
|
declare const ViewLauncher: ({ session, }: {
|
|
9
|
-
session:
|
|
10
|
-
}) => import("react
|
|
3
|
+
session: SessionWithDrawerWidgets;
|
|
4
|
+
}) => import("react").JSX.Element;
|
|
11
5
|
export default ViewLauncher;
|
|
@@ -15,12 +15,14 @@ const useStyles = makeStyles()(theme => ({
|
|
|
15
15
|
const ViewLauncher = observer(function ViewLauncher({ session, }) {
|
|
16
16
|
const { classes } = useStyles();
|
|
17
17
|
const { pluginManager } = getEnv(session);
|
|
18
|
-
const viewTypes = pluginManager
|
|
19
|
-
|
|
18
|
+
const viewTypes = pluginManager
|
|
19
|
+
.getViewElements()
|
|
20
|
+
.filter(({ viewMetadata }) => !viewMetadata.hiddenFromGUI);
|
|
21
|
+
const [value, setValue] = useState(viewTypes[0]?.name ?? '');
|
|
20
22
|
return (_jsxs(Paper, { className: classes.selectPaper, children: [_jsx(Typography, { children: "Select a view to launch" }), _jsx(FormControl, { className: classes.m2, children: _jsx(Select, { value: value, onChange: event => {
|
|
21
23
|
setValue(event.target.value);
|
|
22
|
-
}, children: viewTypes
|
|
23
|
-
.
|
|
24
|
-
|
|
24
|
+
}, children: viewTypes.map(({ displayName, name }) => (_jsx(MenuItem, { value: name, children: displayName }, name))) }) }), _jsx(FormControl, { className: classes.m2, children: _jsx(Button, { onClick: () => {
|
|
25
|
+
void session.launchView(value, {});
|
|
26
|
+
}, variant: "contained", color: "primary", children: "Launch view" }) })] }));
|
|
25
27
|
});
|
|
26
28
|
export default ViewLauncher;
|
package/esm/ui/App/ViewMenu.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
|
|
2
|
-
|
|
3
|
-
declare const ViewMenu: ({ model, IconProps, }: {
|
|
2
|
+
declare const ViewMenu: ({ model, className, }: {
|
|
4
3
|
model: IBaseViewModel;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}) => import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
className?: string;
|
|
5
|
+
}) => import("react").JSX.Element;
|
|
8
6
|
export default ViewMenu;
|