@jbrowse/app-core 4.3.0 → 5.0.0-beta.10

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 (165) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1324 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +294 -0
  8. package/esm/Assemblies/AssembliesMixin.js +154 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +11 -0
  12. package/esm/HistoryManagement/index.js +43 -10
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +79 -7
  15. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  16. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  17. package/esm/JBrowseConfig/index.d.ts +175 -84
  18. package/esm/JBrowseConfig/index.js +64 -2
  19. package/esm/JBrowseModel/index.d.ts +175 -97
  20. package/esm/JBrowseModel/index.js +132 -31
  21. package/esm/JbApi/index.d.ts +2 -0
  22. package/esm/JbApi/index.js +1 -0
  23. package/esm/JbApi/jbApi.d.ts +2767 -0
  24. package/esm/JbApi/jbApi.js +1383 -0
  25. package/esm/RootMenu/index.d.ts +69 -3
  26. package/esm/RootMenu/index.js +96 -61
  27. package/esm/SessionSpec/index.d.ts +6 -0
  28. package/esm/SessionSpec/index.js +3 -0
  29. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  30. package/esm/SessionSpec/lgvUrlInit.js +161 -0
  31. package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
  32. package/esm/SessionSpec/loadSessionSpec.js +473 -0
  33. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
  34. package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
  35. package/esm/SessionSpec/types.d.ts +37 -0
  36. package/esm/SessionSpec/types.js +1 -0
  37. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  38. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  39. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  40. package/esm/WorkspaceLayout/PanelView.js +99 -0
  41. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  42. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  43. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  44. package/esm/WorkspaceLayout/WorkspaceContainer.js +107 -0
  45. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  46. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  47. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  48. package/esm/WorkspaceLayout/WorkspaceTab.js +105 -0
  49. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  50. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  51. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  52. package/esm/WorkspaceLayout/dropZone.js +113 -0
  53. package/esm/WorkspaceLayout/index.d.ts +8 -0
  54. package/esm/WorkspaceLayout/index.js +4 -0
  55. package/esm/WorkspaceLayout/model.d.ts +260 -0
  56. package/esm/WorkspaceLayout/model.js +586 -0
  57. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  58. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  59. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  60. package/esm/WorkspaceLayout/spec.js +226 -0
  61. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  62. package/esm/WorkspaceLayout/splitter.js +37 -0
  63. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  64. package/esm/WorkspaceLayout/tree.js +379 -0
  65. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  66. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  67. package/esm/index.d.ts +18 -9
  68. package/esm/index.js +16 -9
  69. package/esm/menus.d.ts +55 -68
  70. package/esm/menus.js +117 -86
  71. package/esm/queryParams.d.ts +9 -0
  72. package/esm/queryParams.js +93 -0
  73. package/esm/sessionModelFactory.d.ts +18 -0
  74. package/esm/sessionModelFactory.js +1 -0
  75. package/esm/ui/App/App.d.ts +3 -14
  76. package/esm/ui/App/App.js +39 -19
  77. package/esm/ui/App/AppFab.d.ts +12 -3
  78. package/esm/ui/App/AppFab.js +15 -8
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +45 -31
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  88. package/esm/ui/App/DrawerHeader.js +24 -11
  89. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  90. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  91. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  92. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  93. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  94. package/esm/ui/App/DrawerWidget.js +9 -21
  95. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  96. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  97. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  98. package/esm/ui/App/ThemeModeButton.js +34 -0
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +128 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +21 -11
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +48 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +4 -11
  118. package/esm/ui/App/ViewsContainer.js +6 -9
  119. package/esm/ui/App/index.d.ts +2 -1
  120. package/esm/ui/App/index.js +3 -2
  121. package/esm/ui/App/lazyChunk.d.ts +13 -0
  122. package/esm/ui/App/lazyChunk.js +91 -0
  123. package/esm/ui/App/lazyParts.d.ts +32 -0
  124. package/esm/ui/App/lazyParts.js +8 -0
  125. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  126. package/esm/ui/App/preloadAppShell.js +41 -0
  127. package/esm/ui/App/types.d.ts +17 -3
  128. package/esm/ui/App/types.js +1 -1
  129. package/esm/ui/App/useViewVisibility.d.ts +35 -0
  130. package/esm/ui/App/useViewVisibility.js +88 -0
  131. package/esm/ui/App/viewTitle.d.ts +12 -0
  132. package/esm/ui/App/viewTitle.js +17 -0
  133. package/esm/ui/index.d.ts +1 -1
  134. package/esm/ui/index.js +1 -1
  135. package/package.json +12 -17
  136. package/esm/AppFocus/index.d.ts +0 -5
  137. package/esm/AppFocus/index.js +0 -12
  138. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  139. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  140. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  141. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  142. package/esm/DockviewLayout/index.d.ts +0 -35
  143. package/esm/DockviewLayout/index.js +0 -111
  144. package/esm/ui/App/DockviewContext.d.ts +0 -22
  145. package/esm/ui/App/DockviewContext.js +0 -29
  146. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  147. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  148. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  149. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  150. package/esm/ui/App/DrawerControls.d.ts +0 -5
  151. package/esm/ui/App/DrawerControls.js +0 -25
  152. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  153. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  154. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  155. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  156. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  157. package/esm/ui/App/JBrowseViewTab.js +0 -97
  158. package/esm/ui/App/ModalWidget.d.ts +0 -6
  159. package/esm/ui/App/ModalWidget.js +0 -48
  160. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  161. package/esm/ui/App/TiledViewsContainer.js +0 -366
  162. package/esm/ui/App/copyView.d.ts +0 -1
  163. package/esm/ui/App/copyView.js +0 -29
  164. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  165. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { CascadingMenuButton } from '@jbrowse/core/ui';
3
3
  import { getSession } from '@jbrowse/core/util';
4
+ import { renameIds } from '@jbrowse/core/util/types/mst';
4
5
  import { getSnapshot } from '@jbrowse/mobx-state-tree';
5
6
  import ContentCopyIcon from '@mui/icons-material/ContentCopy';
6
7
  import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
@@ -11,106 +12,100 @@ import MenuIcon from '@mui/icons-material/Menu';
11
12
  import OpenInNewIcon from '@mui/icons-material/OpenInNew';
12
13
  import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
13
14
  import { observer } from 'mobx-react';
14
- import { useDockview } from "./DockviewContext.js";
15
- import { renameIds } from "./copyView.js";
16
- import { isSessionWithDockviewLayout } from "../../DockviewLayout/index.js";
17
- function getPanelViewCount(session, viewId) {
18
- if (!isSessionWithDockviewLayout(session)) {
19
- return 0;
20
- }
21
- for (const viewIds of session.panelViewAssignments.values()) {
22
- if (viewIds.includes(viewId)) {
23
- return viewIds.length;
24
- }
25
- }
26
- return 0;
27
- }
28
- const ViewMenu = observer(function ViewMenu({ model, IconProps, }) {
15
+ // takes the icon's class, not an SvgIconProps object: the object was built
16
+ // inline by ViewHeader and so was new on each of its renders, which defeated
17
+ // mobx-react's memo for this whole menu (a Tooltip and an IconButton) every
18
+ // time anything re-rendered the header
19
+ const ViewMenu = observer(function ViewMenu({ model, className, }) {
29
20
  const session = getSession(model);
30
- const { moveViewToNewTab, moveViewToSplitRight } = useDockview();
31
- const usePanel = session.useWorkspaces && isSessionWithDockviewLayout(session);
32
- const viewCount = usePanel
33
- ? getPanelViewCount(session, model.id)
34
- : session.views.length;
35
- const moveView = (panelFn, sessionFn) => {
36
- if (usePanel) {
37
- panelFn(model.id);
21
+ const moves = {
22
+ top: session.moveViewToTop,
23
+ up: session.moveViewUp,
24
+ down: session.moveViewDown,
25
+ bottom: session.moveViewToBottom,
26
+ };
27
+ // Give this view a home of its own: its own tab beside the rest, or its own
28
+ // split to their right. One path, whether or not the workspace is already up
29
+ // — the layout is session state either way, so there is nothing to defer and
30
+ // no second implementation for "the panels do not exist yet". That fork is
31
+ // the reason `init` existed.
32
+ const moveViewOut = (direction) => {
33
+ const allViewIds = session.views.map(v => v.id);
34
+ if (direction === 'tabs') {
35
+ session.moveViewToNewTab(model.id, allViewIds);
38
36
  }
39
37
  else {
40
- sessionFn(model.id);
38
+ session.moveViewToSplitRight(model.id, allViewIds);
41
39
  }
40
+ session.setUseWorkspaces(true);
42
41
  };
43
- return (_jsx(CascadingMenuButton, { "data-testid": "view_menu_icon", menuItems: () => [
44
- {
45
- label: 'View options',
46
- type: 'subMenu',
47
- subMenu: [
48
- {
49
- label: 'Copy view',
50
- icon: ContentCopyIcon,
51
- onClick: () => {
52
- session.addView(model.type, renameIds(structuredClone(getSnapshot(model))));
53
- },
54
- },
55
- {
56
- label: 'Move to new tab',
57
- icon: OpenInNewIcon,
58
- onClick: () => {
59
- moveViewToNewTab(model.id);
60
- session.setUseWorkspaces(true);
61
- },
62
- },
63
- {
64
- label: 'Move to split view (right side of screen)',
65
- icon: VerticalSplitIcon,
66
- onClick: () => {
67
- moveViewToSplitRight(model.id);
68
- session.setUseWorkspaces(true);
69
- },
70
- },
71
- ...(viewCount > 2
72
- ? [
73
- {
74
- label: 'Move view to top',
75
- icon: KeyboardDoubleArrowUpIcon,
76
- onClick: () => {
77
- moveView(session.moveViewToTopInPanel, session.moveViewToTop);
78
- },
42
+ return (_jsx(CascadingMenuButton, { "data-testid": "view_menu_icon", tooltip: "View menu", menuItems: () => {
43
+ // The views this move is relative to: in a workspace, the ones sharing
44
+ // this view's panel; in the classic stack, all of them. `session.views`
45
+ // is the order either way, so the mode decides the SCOPE of a move and
46
+ // nothing else. There is one implementation of "move a view" again.
47
+ //
48
+ // Resolved here rather than during render. It scans every panel's
49
+ // assignment list and copies one of them, and reading those lists in
50
+ // render would also subscribe this menu to them, so a view moving
51
+ // between panels anywhere re-rendered every view's menu. None of it is
52
+ // needed until the menu opens.
53
+ const scopeIds = session.effectiveUseWorkspaces
54
+ ? session.tabContainingView(model.id)?.tab.viewIds.slice()
55
+ : undefined;
56
+ const viewCount = scopeIds?.length ?? session.views.length;
57
+ return [
58
+ {
59
+ label: 'View options',
60
+ type: 'subMenu',
61
+ subMenu: [
62
+ {
63
+ label: 'Copy view',
64
+ icon: ContentCopyIcon,
65
+ onClick: () => {
66
+ session.addView(model.type, renameIds(getSnapshot(model)));
79
67
  },
80
- ]
81
- : []),
82
- ...(viewCount > 1
83
- ? [
84
- {
85
- label: 'Move view up',
86
- icon: KeyboardArrowUpIcon,
87
- onClick: () => {
88
- moveView(session.moveViewUpInPanel, session.moveViewUp);
89
- },
68
+ },
69
+ {
70
+ label: 'Move to new tab',
71
+ icon: OpenInNewIcon,
72
+ onClick: () => {
73
+ moveViewOut('tabs');
90
74
  },
91
- {
92
- label: 'Move view down',
93
- icon: KeyboardArrowDownIcon,
94
- onClick: () => {
95
- moveView(session.moveViewDownInPanel, session.moveViewDown);
96
- },
75
+ },
76
+ {
77
+ label: 'Move to split view (right side of screen)',
78
+ icon: VerticalSplitIcon,
79
+ onClick: () => {
80
+ moveViewOut('horizontal');
97
81
  },
98
- ]
99
- : []),
100
- ...(viewCount > 2
101
- ? [
102
- {
103
- label: 'Move view to bottom',
104
- icon: KeyboardDoubleArrowDownIcon,
105
- onClick: () => {
106
- moveView(session.moveViewToBottomInPanel, session.moveViewToBottom);
82
+ },
83
+ // 'top'/'bottom' only mean something with a view above *and* below
84
+ ...[
85
+ ['top', 'Move view to top', KeyboardDoubleArrowUpIcon, 2],
86
+ ['up', 'Move view up', KeyboardArrowUpIcon, 1],
87
+ ['down', 'Move view down', KeyboardArrowDownIcon, 1],
88
+ [
89
+ 'bottom',
90
+ 'Move view to bottom',
91
+ KeyboardDoubleArrowDownIcon,
92
+ 2,
93
+ ],
94
+ ].flatMap(([direction, label, icon, minViews]) => viewCount > minViews
95
+ ? [
96
+ {
97
+ label,
98
+ icon,
99
+ onClick: () => {
100
+ moves[direction](model.id, scopeIds);
101
+ },
107
102
  },
108
- },
109
- ]
110
- : []),
111
- ],
112
- },
113
- ...model.menuItems(),
114
- ], children: _jsx(MenuIcon, { ...IconProps, fontSize: "small" }) }));
103
+ ]
104
+ : []),
105
+ ],
106
+ },
107
+ ...model.menuItems(),
108
+ ];
109
+ }, children: _jsx(MenuIcon, { className: className, fontSize: "small" }) }));
115
110
  });
116
111
  export default ViewMenu;
@@ -0,0 +1,7 @@
1
+ import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
2
+ declare const ViewStack: ({ views, session, className, }: {
3
+ views: AbstractViewModel[];
4
+ session: SessionWithFocusedViewAndDrawerWidgets;
5
+ className?: string;
6
+ }) => import("react").JSX.Element;
7
+ export default ViewStack;
@@ -0,0 +1,48 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { SCROLL_PORT_HEIGHT_VAR, useScrollPortOverflow, } from '@jbrowse/core/util/hooks';
4
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
5
+ import { observer } from 'mobx-react';
6
+ import ViewContainer from './ViewContainer.js';
7
+ // What the trailing space is *for*, which is what sets it: `ViewHeader` brings a
8
+ // newly-added view in with `scrollIntoView({ block: 'center' })`, and centering
9
+ // the last view needs half a port of room below it. Half the port is therefore
10
+ // the amount that makes that gesture reach, and any more is room to scroll the
11
+ // content off its own surface.
12
+ //
13
+ // It is scrollable room rather than content, so anything measuring how tall the
14
+ // session is discounts it by its `view-stack-overscroll` testid — `overflow` in
15
+ // jbApi does, and a filmed take paid for that: counted as content, it told
16
+ // `jb.fitToWindow` the session was 300px taller than it was, and the dotplot
17
+ // absorbing the cut came out a third of the height that fit.
18
+ //
19
+ // Capped, because half a port stops being a courtesy on a tall screen. 300px is
20
+ // the cap the classic stack has always had and it stays the answer there — a
21
+ // full window is comfortably past twice it. What the cap was hiding is the
22
+ // workspace panel, where a 2x2 grid's cell can be shorter than 600px and a flat
23
+ // 300px let a view be dragged nearly clear of its own cell.
24
+ const MAX_OVERSCROLL = 300;
25
+ const useStyles = makeStyles()({
26
+ // trailing space so the last view can scroll up past the bottom of a
27
+ // container that would otherwise clip its lower edge.
28
+ //
29
+ // Rendered only once the views themselves overflow, and that is what makes
30
+ // the port's scrollbar honest: kept unconditionally, this alone overflowed
31
+ // the port, so every session — a single short view included — scrolled into
32
+ // nothing and drew a scrollbar saying so. It is outside the measured element
33
+ // for the same reason (see `useScrollPortOverflow`).
34
+ spacer: {
35
+ height: `min(${MAX_OVERSCROLL}px, calc(var(${SCROLL_PORT_HEIGHT_VAR}, 100vh) / 2))`,
36
+ },
37
+ });
38
+ const ViewStack = observer(function ViewStack({ views, session, className, }) {
39
+ const { classes } = useStyles();
40
+ const viewsRef = useRef(null);
41
+ const scrollable = useScrollPortOverflow(viewsRef);
42
+ // View ids present at first render: these arrived with the page (session
43
+ // restore / initial load), so they must not steal scroll when they mount. A
44
+ // view whose id isn't in this set was added later and auto-scrolls into view.
45
+ const [initialViewIds] = useState(() => new Set(views.map(v => v.id)));
46
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: viewsRef, className: className, children: views.map(view => (_jsx(ViewContainer, { view: view, session: session, scrollOnMount: !initialViewIds.has(view.id) }, view.id))) }), scrollable ? (_jsx("div", { className: classes.spacer, "data-testid": "view-stack-overscroll" })) : null] }));
47
+ });
48
+ export default ViewStack;
@@ -1,6 +1,20 @@
1
1
  import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
2
+ /**
3
+ * A view's body, and the boundary that keeps a view that throws from being an
4
+ * application-wide failure.
5
+ *
6
+ * Without it the next boundary up is the product's own — jbrowse-web's is in
7
+ * `Loader.tsx` and its fallback is `FatalErrorDialog`, so one bad view replaced
8
+ * every other view the user had open. The reachable case is not hypothetical: a
9
+ * display's reads walk to `getContainingView` and throw when the walk finds no
10
+ * view, which is what ADR-069 is about.
11
+ *
12
+ * No reset keys: a view body unmounts on its own when the view is minimized or
13
+ * scrolls out of the mount band, so remounting is already how a stale banner
14
+ * clears. What the fallback adds is the deliberate ways out — retry, and close.
15
+ */
2
16
  declare const ViewWrapper: ({ view, session, }: {
3
17
  view: AbstractViewModel;
4
18
  session: SessionWithFocusedViewAndDrawerWidgets;
5
- }) => import("react/jsx-runtime").JSX.Element | null;
19
+ }) => import("react").JSX.Element;
6
20
  export default ViewWrapper;
@@ -1,18 +1,34 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Suspense } from 'react';
3
+ import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary';
3
4
  import LoadingEllipses from '@jbrowse/core/ui/LoadingEllipses';
4
5
  import { getEnv } from '@jbrowse/core/util';
5
6
  import { observer } from 'mobx-react';
6
- const ViewWrapper = observer(function ViewWrapper({ view, session, }) {
7
+ import ViewErrorFallback from './ViewErrorFallback.js';
8
+ // Separate from the boundary below on purpose: `getViewType` throws when no
9
+ // plugin registered the type, and a lookup done in the boundary's own render is
10
+ // a lookup the boundary cannot catch. This is the one thing in a view's render
11
+ // path that fails without any of the view's own code running.
12
+ const ViewComponent = observer(function ViewComponent({ view, session, }) {
7
13
  const { pluginManager } = getEnv(session);
8
- const viewType = pluginManager.getViewType(view.type);
9
- if (!viewType) {
10
- throw new Error(`unknown view type ${view.type}`);
11
- }
12
- const { ReactComponent } = viewType;
13
- if (view.minimized) {
14
- return null;
15
- }
16
- return (_jsx(Suspense, { fallback: _jsx(LoadingEllipses, { variant: "h6" }), children: _jsx(ReactComponent, { model: view, session: session }) }));
14
+ const { ReactComponent } = pluginManager.getViewType(view.type);
15
+ return (_jsx(Suspense, { fallback: _jsx(LoadingEllipses, { variant: "h6", "data-view-component-pending": "true" }), children: _jsx(ReactComponent, { model: view, session: session }) }));
16
+ });
17
+ /**
18
+ * A view's body, and the boundary that keeps a view that throws from being an
19
+ * application-wide failure.
20
+ *
21
+ * Without it the next boundary up is the product's own — jbrowse-web's is in
22
+ * `Loader.tsx` and its fallback is `FatalErrorDialog`, so one bad view replaced
23
+ * every other view the user had open. The reachable case is not hypothetical: a
24
+ * display's reads walk to `getContainingView` and throw when the walk finds no
25
+ * view, which is what ADR-069 is about.
26
+ *
27
+ * No reset keys: a view body unmounts on its own when the view is minimized or
28
+ * scrolls out of the mount band, so remounting is already how a stale banner
29
+ * clears. What the fallback adds is the deliberate ways out — retry, and close.
30
+ */
31
+ const ViewWrapper = observer(function ViewWrapper({ view, session, }) {
32
+ return (_jsx(ErrorBoundary, { FallbackComponent: ({ error, resetErrorBoundary }) => (_jsx(ViewErrorFallback, { view: view, session: session, error: error, onRetry: resetErrorBoundary })), children: _jsx(ViewComponent, { view: view, session: session }) }));
17
33
  });
18
34
  export default ViewWrapper;
@@ -1,12 +1,5 @@
1
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
2
- import type { AbstractViewContainer, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
3
- interface Props {
4
- HeaderButtons?: React.ReactElement;
5
- session: SessionWithFocusedViewAndDrawerWidgets & AbstractViewContainer & {
6
- renameCurrentSession: (arg: string) => void;
7
- snackbarMessages: SnackbarMessage[];
8
- popSnackbarMessage: () => unknown;
9
- };
10
- }
11
- declare const ViewsContainer: (props: Props) => import("react/jsx-runtime").JSX.Element;
1
+ import type { AppSession } from './types.ts';
2
+ declare const ViewsContainer: ({ session, }: {
3
+ session: AppSession;
4
+ }) => import("react").JSX.Element;
12
5
  export default ViewsContainer;
@@ -1,21 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Suspense, lazy } from 'react';
2
+ import { Suspense } from 'react';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
4
  import { observer } from 'mobx-react';
5
- const ClassicViewsContainer = lazy(() => import("./ClassicViewsContainer.js"));
6
- const TiledViewsContainer = lazy(() => import("./TiledViewsContainer.js"));
7
- const ViewLauncher = lazy(() => import("./ViewLauncher.js"));
5
+ import { isSessionWithWorkspaceLayout } from '../../WorkspaceLayout/model.js';
6
+ import { ClassicViewsContainer, ViewLauncher, WorkspaceContainer, } from './lazyParts.js';
8
7
  const useStyles = makeStyles()({
9
8
  viewsContainer: {
10
9
  gridRow: 'components',
11
10
  overflow: 'hidden',
12
11
  },
13
12
  });
14
- const ViewsContainer = observer(function ViewsContainer(props) {
15
- const { session } = props;
16
- const { views } = session;
13
+ const ViewsContainer = observer(function ViewsContainer({ session, }) {
14
+ const { views, effectiveUseWorkspaces } = session;
17
15
  const { classes } = useStyles();
18
- const useWorkspaces = 'useWorkspaces' in session ? session.useWorkspaces : true;
19
- return (_jsx("div", { className: classes.viewsContainer, children: views.length > 0 ? (_jsx(Suspense, { fallback: null, children: useWorkspaces ? (_jsx(TiledViewsContainer, { session: session })) : (_jsx(ClassicViewsContainer, { session: session })) })) : (_jsx(Suspense, { fallback: null, children: _jsx(ViewLauncher, { ...props }) })) }));
16
+ return (_jsx("div", { className: classes.viewsContainer, children: _jsx(Suspense, { fallback: null, children: views.length > 0 ? (effectiveUseWorkspaces && isSessionWithWorkspaceLayout(session) ? (_jsx(WorkspaceContainer, { session: session })) : (_jsx(ClassicViewsContainer, { session: session }))) : (_jsx(ViewLauncher, { session: session })) }) }));
20
17
  });
21
18
  export default ViewsContainer;
@@ -1,2 +1,3 @@
1
- export * from './App.tsx';
1
+ export { App } from './App.tsx';
2
2
  export { default as DialogQueue } from './DialogQueue.tsx';
3
+ export { preloadAppFrame, preloadAppShell } from './preloadAppShell.ts';
@@ -1,2 +1,3 @@
1
- export * from "./App.js";
2
- export { default as DialogQueue } from "./DialogQueue.js";
1
+ export { App } from './App.js';
2
+ export { default as DialogQueue } from './DialogQueue.js';
3
+ export { preloadAppFrame, preloadAppShell } from './preloadAppShell.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A dynamic import that survives one bad response for its chunk.
3
+ *
4
+ * The retry is not a hopeful second roll: `probeChunk` refetches with
5
+ * `cache: 'reload'`, which both reports what the server really says and
6
+ * replaces whatever the HTTP cache was holding, and webpack clears its
7
+ * `installedChunks` entry on failure — so the second `import()` is a real
8
+ * request against a repaired cache entry. React.lazy cannot do this itself: it
9
+ * stores the rejection and rethrows it for the life of the page without ever
10
+ * calling the factory again, so the recovery has to happen here, before the
11
+ * promise settles.
12
+ */
13
+ export declare function lazyChunk<T>(name: string, load: () => Promise<T>): () => Promise<T>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Wraps a dynamic import so a failed chunk fetch says what actually happened.
3
+ *
4
+ * Webpack flattens every network-level failure into the same string —
5
+ * `Loading chunk X failed.\n(error: <url>)` — where `error` only means the
6
+ * script element fired `error` rather than `load` (a 404, a refused
7
+ * connection and a dropped socket are indistinguishable). That string is what
8
+ * jbrowse-web records via `markCrashedSession`, so it is also all a later boot
9
+ * can show. The immediate refetch below is the part that discriminates:
10
+ * webpack clears its `installedChunks` entry on failure, so the asset is
11
+ * really re-requested, and a 200 here means the asset was always fine and the
12
+ * one request was not.
13
+ */
14
+ async function probeChunk(url) {
15
+ try {
16
+ const response = await fetch(url, { cache: 'reload' });
17
+ const body = await response.text();
18
+ return `refetch: HTTP ${response.status} ${response.statusText}, ${response.headers.get('content-type')}, ${body.length} bytes`;
19
+ }
20
+ catch (e) {
21
+ return `refetch also failed: ${e}`;
22
+ }
23
+ }
24
+ /**
25
+ * What the browser recorded for the request that failed — the status it
26
+ * actually saw and whether any bytes crossed the network. A `transferSize` of
27
+ * 0 alongside a body size says the response came from the cache rather than
28
+ * the server, which is the one thing `curl` can never show.
29
+ */
30
+ function resourceTiming(url) {
31
+ const entry = performance
32
+ .getEntriesByName(url)
33
+ .find(e => e instanceof PerformanceResourceTiming);
34
+ return entry
35
+ ? {
36
+ responseStatus: entry.responseStatus,
37
+ transferSize: entry.transferSize,
38
+ encodedBodySize: entry.encodedBodySize,
39
+ duration: Math.round(entry.duration),
40
+ }
41
+ : 'no resource timing entry';
42
+ }
43
+ /**
44
+ * The two fields webpack hangs off a ChunkLoadError: the URL it asked for, and
45
+ * whether the script element reported `error` (the request failed), `missing`
46
+ * (it loaded but registered nothing) or `timeout`.
47
+ */
48
+ function chunkErrorDetail(error) {
49
+ return error instanceof Error &&
50
+ error.name === 'ChunkLoadError' &&
51
+ 'request' in error &&
52
+ typeof error.request === 'string'
53
+ ? {
54
+ url: error.request,
55
+ type: 'type' in error ? String(error.type) : 'unknown',
56
+ }
57
+ : undefined;
58
+ }
59
+ /**
60
+ * A dynamic import that survives one bad response for its chunk.
61
+ *
62
+ * The retry is not a hopeful second roll: `probeChunk` refetches with
63
+ * `cache: 'reload'`, which both reports what the server really says and
64
+ * replaces whatever the HTTP cache was holding, and webpack clears its
65
+ * `installedChunks` entry on failure — so the second `import()` is a real
66
+ * request against a repaired cache entry. React.lazy cannot do this itself: it
67
+ * stores the rejection and rethrows it for the life of the page without ever
68
+ * calling the factory again, so the recovery has to happen here, before the
69
+ * promise settles.
70
+ */
71
+ export function lazyChunk(name, load) {
72
+ return () => load().catch(async (error) => {
73
+ const detail = chunkErrorDetail(error);
74
+ if (detail) {
75
+ console.error(`chunk load failed: ${name}`, JSON.stringify({
76
+ ...detail,
77
+ online: navigator.onLine,
78
+ at: new Date().toISOString(),
79
+ timing: resourceTiming(detail.url),
80
+ probe: await probeChunk(detail.url),
81
+ }, undefined, 2));
82
+ const retried = await load().catch((retryError) => {
83
+ console.error(`chunk retry failed: ${name}`, retryError);
84
+ throw error;
85
+ });
86
+ console.error(`chunk retry succeeded: ${name}`);
87
+ return retried;
88
+ }
89
+ throw error;
90
+ });
91
+ }
@@ -0,0 +1,32 @@
1
+ export declare const DrawerWidget: import("react").LazyExoticComponent<({ session, }: {
2
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets;
3
+ }) => import("react").JSX.Element> & {
4
+ preload: () => Promise<{
5
+ default: ({ session, }: {
6
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets;
7
+ }) => import("react").JSX.Element;
8
+ }>;
9
+ };
10
+ export declare const ClassicViewsContainer: import("react").LazyExoticComponent<({ session, }: import("./ClassicViewsContainer.tsx").ClassicViewsContainerProps) => import("react").JSX.Element> & {
11
+ preload: () => Promise<{
12
+ default: ({ session, }: import("./ClassicViewsContainer.tsx").ClassicViewsContainerProps) => import("react").JSX.Element;
13
+ }>;
14
+ };
15
+ export declare const WorkspaceContainer: import("react").LazyExoticComponent<({ session, }: {
16
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets & import("../../index.ts").WorkspaceLayout;
17
+ }) => import("react").JSX.Element> & {
18
+ preload: () => Promise<{
19
+ default: ({ session, }: {
20
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets & import("../../index.ts").WorkspaceLayout;
21
+ }) => import("react").JSX.Element;
22
+ }>;
23
+ };
24
+ export declare const ViewLauncher: import("react").LazyExoticComponent<({ session, }: {
25
+ session: import("@jbrowse/core/util").SessionWithDrawerWidgets;
26
+ }) => import("react").JSX.Element> & {
27
+ preload: () => Promise<{
28
+ default: ({ session, }: {
29
+ session: import("@jbrowse/core/util").SessionWithDrawerWidgets;
30
+ }) => import("react").JSX.Element;
31
+ }>;
32
+ };
@@ -0,0 +1,8 @@
1
+ import { lazyWithPreload } from '@jbrowse/core/util/lazyWithPreload';
2
+ import { lazyChunk } from './lazyChunk.js';
3
+ export const DrawerWidget = lazyWithPreload(lazyChunk('DrawerWidget', () => import('./DrawerWidget.js')));
4
+ export const ClassicViewsContainer = lazyWithPreload(lazyChunk('ClassicViewsContainer', () => import('./ClassicViewsContainer.js')));
5
+ export const WorkspaceContainer = lazyWithPreload(lazyChunk('WorkspaceContainer', () => import('../../WorkspaceLayout/WorkspaceContainer.js').then(m => ({
6
+ default: m.WorkspaceContainer,
7
+ }))));
8
+ export const ViewLauncher = lazyWithPreload(lazyChunk('ViewLauncher', () => import('./ViewLauncher.js')));
@@ -0,0 +1,14 @@
1
+ import type { AppSession } from './types.ts';
2
+ /**
3
+ * Load the views container and the drawer while the session's own view code
4
+ * downloads, settling when both have. An app that renders after this renders
5
+ * its frame in one pass; otherwise the first render suspends on them and React
6
+ * holds the retry for 300 ms.
7
+ */
8
+ export declare function preloadAppFrame(): Promise<void>;
9
+ /**
10
+ * Request the app frame's lazy parts that `session` will show (its views
11
+ * container, the drawer and the widget open in it) as soon as the session
12
+ * exists. Rendering asks for each only when it reaches it, a round trip apiece.
13
+ */
14
+ export declare function preloadAppShell(session: AppSession): void;
@@ -0,0 +1,41 @@
1
+ import { preloadComponent } from '@jbrowse/core/util/preloadComponent';
2
+ import { getEnv } from '@jbrowse/mobx-state-tree';
3
+ import { isSessionWithWorkspaceLayout } from '../../WorkspaceLayout/model.js';
4
+ import { ClassicViewsContainer, DrawerWidget, WorkspaceContainer, } from './lazyParts.js';
5
+ /**
6
+ * Load the views container and the drawer while the session's own view code
7
+ * downloads, settling when both have. An app that renders after this renders
8
+ * its frame in one pass; otherwise the first render suspends on them and React
9
+ * holds the retry for 300 ms.
10
+ */
11
+ export async function preloadAppFrame() {
12
+ await Promise.allSettled([
13
+ ClassicViewsContainer.preload(),
14
+ DrawerWidget.preload(),
15
+ ]);
16
+ }
17
+ /**
18
+ * Request the app frame's lazy parts that `session` will show (its views
19
+ * container, the drawer and the widget open in it) as soon as the session
20
+ * exists. Rendering asks for each only when it reaches it, a round trip apiece.
21
+ */
22
+ export function preloadAppShell(session) {
23
+ if (session.views.length > 0) {
24
+ preloadComponent(session.effectiveUseWorkspaces && isSessionWithWorkspaceLayout(session)
25
+ ? WorkspaceContainer
26
+ : ClassicViewsContainer);
27
+ }
28
+ const { drawerVisible, visibleWidget } = session;
29
+ if (drawerVisible) {
30
+ preloadComponent(DrawerWidget);
31
+ }
32
+ if (drawerVisible && visibleWidget) {
33
+ const { pluginManager } = getEnv(session);
34
+ try {
35
+ preloadComponent(pluginManager.getWidgetType(visibleWidget.type).ReactComponent);
36
+ }
37
+ catch {
38
+ // an unknown widget type is reported when the drawer renders it
39
+ }
40
+ }
41
+ }
@@ -1,6 +1,20 @@
1
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
2
- import type { AbstractViewContainer, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
3
- export type DockviewSessionType = SessionWithFocusedViewAndDrawerWidgets & AbstractViewContainer & {
1
+ import type { Menu } from '../../menus.ts';
2
+ import type { ErrorDialogState, SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
3
+ import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
4
+ export { type Menu } from '../../menus.ts';
5
+ export interface ThemeSwitchSession {
6
+ themeMode: 'light' | 'dark' | 'system';
7
+ effectiveThemeMode: 'light' | 'dark';
8
+ themeIsDark: boolean;
9
+ systemThemeOverride?: 'light' | 'dark';
10
+ setSystemThemeOverride: (mode?: 'light' | 'dark') => void;
11
+ }
12
+ export type WorkspaceSessionType = SessionWithFocusedViewAndDrawerWidgets;
13
+ export type AppSession = WorkspaceSessionType & ThemeSwitchSession & {
14
+ menus: () => Menu[];
15
+ errorDialog: ErrorDialogState | undefined;
16
+ setErrorDialog: (state: ErrorDialogState | undefined) => void;
17
+ effectiveUseWorkspaces: boolean;
4
18
  renameCurrentSession: (arg: string) => void;
5
19
  snackbarMessages: SnackbarMessage[];
6
20
  popSnackbarMessage: () => unknown;
@@ -1 +1 @@
1
- export {};
1
+ export {} from '../../menus.js';