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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) 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 +1598 -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 +225 -0
  8. package/esm/Assemblies/AssembliesMixin.js +149 -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 +19 -0
  12. package/esm/HistoryManagement/index.js +42 -5
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +167 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +85 -7
  15. package/esm/JBrowseConfig/index.d.ts +169 -84
  16. package/esm/JBrowseConfig/index.js +62 -2
  17. package/esm/JBrowseModel/index.d.ts +176 -95
  18. package/esm/JBrowseModel/index.js +122 -21
  19. package/esm/RootMenu/index.d.ts +77 -2
  20. package/esm/RootMenu/index.js +107 -61
  21. package/esm/SessionSpec/index.d.ts +6 -0
  22. package/esm/SessionSpec/index.js +3 -0
  23. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  24. package/esm/SessionSpec/lgvUrlInit.js +158 -0
  25. package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
  26. package/esm/SessionSpec/loadSessionSpec.js +311 -0
  27. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
  28. package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
  29. package/esm/SessionSpec/types.d.ts +33 -0
  30. package/esm/SessionSpec/types.js +1 -0
  31. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  32. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  33. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  34. package/esm/WorkspaceLayout/PanelView.js +99 -0
  35. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  36. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  37. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  38. package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
  39. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  40. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  41. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  42. package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
  43. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  44. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  45. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  46. package/esm/WorkspaceLayout/dropZone.js +113 -0
  47. package/esm/WorkspaceLayout/index.d.ts +8 -0
  48. package/esm/WorkspaceLayout/index.js +4 -0
  49. package/esm/WorkspaceLayout/model.d.ts +240 -0
  50. package/esm/WorkspaceLayout/model.js +527 -0
  51. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  52. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  53. package/esm/WorkspaceLayout/spec.d.ts +58 -0
  54. package/esm/WorkspaceLayout/spec.js +153 -0
  55. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  56. package/esm/WorkspaceLayout/splitter.js +37 -0
  57. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  58. package/esm/WorkspaceLayout/tree.js +379 -0
  59. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  60. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  61. package/esm/index.d.ts +16 -9
  62. package/esm/index.js +13 -9
  63. package/esm/menus.d.ts +63 -62
  64. package/esm/menus.js +131 -84
  65. package/esm/queryParams.d.ts +9 -0
  66. package/esm/queryParams.js +92 -0
  67. package/esm/sessionModelFactory.d.ts +18 -0
  68. package/esm/sessionModelFactory.js +1 -0
  69. package/esm/ui/App/App.d.ts +3 -14
  70. package/esm/ui/App/App.js +30 -17
  71. package/esm/ui/App/AppFab.d.ts +12 -3
  72. package/esm/ui/App/AppFab.js +15 -8
  73. package/esm/ui/App/AppReadyMarker.d.ts +34 -0
  74. package/esm/ui/App/AppReadyMarker.js +56 -0
  75. package/esm/ui/App/AppToolbar.d.ts +2 -14
  76. package/esm/ui/App/AppToolbar.js +23 -24
  77. package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
  78. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  79. package/esm/ui/App/DialogQueue.d.ts +17 -1
  80. package/esm/ui/App/DialogQueue.js +30 -1
  81. package/esm/ui/App/Drawer.d.ts +1 -1
  82. package/esm/ui/App/Drawer.js +10 -39
  83. package/esm/ui/App/DrawerControls.d.ts +1 -5
  84. package/esm/ui/App/DrawerControls.js +1 -25
  85. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  86. package/esm/ui/App/DrawerHeader.js +24 -10
  87. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  88. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  89. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  90. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  91. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  92. package/esm/ui/App/DrawerWidget.js +9 -21
  93. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  94. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  95. package/esm/ui/App/ViewButtons.d.ts +2 -4
  96. package/esm/ui/App/ViewButtons.js +15 -3
  97. package/esm/ui/App/ViewContainer.d.ts +3 -2
  98. package/esm/ui/App/ViewContainer.js +144 -24
  99. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  100. package/esm/ui/App/ViewContainerTitle.js +9 -2
  101. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  102. package/esm/ui/App/ViewErrorFallback.js +42 -0
  103. package/esm/ui/App/ViewHeader.d.ts +3 -4
  104. package/esm/ui/App/ViewHeader.js +28 -17
  105. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  106. package/esm/ui/App/ViewLauncher.js +7 -5
  107. package/esm/ui/App/ViewMenu.d.ts +3 -5
  108. package/esm/ui/App/ViewMenu.js +87 -92
  109. package/esm/ui/App/ViewStack.d.ts +7 -0
  110. package/esm/ui/App/ViewStack.js +42 -0
  111. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  112. package/esm/ui/App/ViewWrapper.js +26 -10
  113. package/esm/ui/App/ViewsContainer.d.ts +3 -8
  114. package/esm/ui/App/ViewsContainer.js +9 -6
  115. package/esm/ui/App/copyView.js +1 -1
  116. package/esm/ui/App/index.d.ts +2 -1
  117. package/esm/ui/App/index.js +3 -2
  118. package/esm/ui/App/lazyChunk.d.ts +13 -0
  119. package/esm/ui/App/lazyChunk.js +91 -0
  120. package/esm/ui/App/types.d.ts +12 -3
  121. package/esm/ui/App/types.js +1 -1
  122. package/esm/ui/App/useViewVisibility.d.ts +34 -0
  123. package/esm/ui/App/useViewVisibility.js +87 -0
  124. package/esm/ui/App/viewTitle.d.ts +5 -0
  125. package/esm/ui/App/viewTitle.js +9 -0
  126. package/esm/ui/index.d.ts +1 -1
  127. package/esm/ui/index.js +1 -1
  128. package/package.json +12 -17
  129. package/esm/AppFocus/index.d.ts +0 -5
  130. package/esm/AppFocus/index.js +0 -12
  131. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  132. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  133. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  134. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  135. package/esm/DockviewLayout/index.d.ts +0 -35
  136. package/esm/DockviewLayout/index.js +0 -111
  137. package/esm/ui/App/DockviewContext.d.ts +0 -22
  138. package/esm/ui/App/DockviewContext.js +0 -29
  139. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  140. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  141. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  142. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  143. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  144. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  145. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  146. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  147. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  148. package/esm/ui/App/JBrowseViewTab.js +0 -97
  149. package/esm/ui/App/ModalWidget.d.ts +0 -6
  150. package/esm/ui/App/ModalWidget.js +0 -48
  151. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  152. package/esm/ui/App/TiledViewsContainer.js +0 -366
  153. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  154. package/esm/ui/App/dockviewUtils.js +0 -55
package/esm/index.js CHANGED
@@ -1,9 +1,13 @@
1
- export * from "./ui/index.js";
2
- export * from "./HistoryManagement/index.js";
3
- export * from "./AppFocus/index.js";
4
- export * from "./JBrowseConfig/index.js";
5
- export * from "./JBrowseModel/index.js";
6
- export * from "./Assemblies/index.js";
7
- export * from "./RootMenu/index.js";
8
- export * from "./DockviewLayout/index.js";
9
- export * from "./menus.js";
1
+ export { App, AppReadyMarker, DialogQueue } from './ui/index.js';
2
+ export { HistoryManagementMixin } from './HistoryManagement/index.js';
3
+ export { DEFAULT_SHARE_URL, JBrowseConfigF } from './JBrowseConfig/index.js';
4
+ export { JBrowseModelF } from './JBrowseModel/index.js';
5
+ export { AssembliesMixin } from './Assemblies/index.js';
6
+ export { AppSessionMixin } from './AppSession/index.js';
7
+ export { RootAppMenuMixin } from './RootMenu/index.js';
8
+ export { WorkspaceLayoutMixin, isSessionWithWorkspaceLayout, } from './WorkspaceLayout/index.js';
9
+ export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.js';
10
+ // URL params, for the two products that own their page (web, desktop). An
11
+ // embedded product must not rewrite its host's URL — see queryParams.ts
12
+ export { deleteQueryParams, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.js';
13
+ export { processMutableMenuActions, resolveMenus } from './menus.js';
package/esm/menus.d.ts CHANGED
@@ -1,74 +1,75 @@
1
- import type { MenuItem } from '@jbrowse/core/ui';
2
- interface InsertInSubMenuAction {
3
- type: 'insertInSubMenu';
4
- menuPath: string[];
5
- menuItem: MenuItem;
6
- position: number;
7
- }
8
- interface InsertInMenuAction {
9
- type: 'insertInMenu';
1
+ import type { MenuItem, MenuItemsGetter } from '@jbrowse/core/ui';
2
+ /**
3
+ * Ensure a top-level menu exists. `position` appends when omitted and counts
4
+ * from the end when negative. A menu whose label is already on the bar is left
5
+ * where it is — labels are the bar's identity (they key the React elements and
6
+ * they are how an item contribution names its target), so two of them is never
7
+ * what a caller wanted.
8
+ */
9
+ interface AddMenuAction {
10
+ type: 'addMenu';
10
11
  menuName: string;
11
- menuItem: MenuItem;
12
- position: number;
13
- }
14
- interface AppendToMenuAction {
15
- type: 'appendToMenu';
16
- menuName: string;
17
- menuItem: MenuItem;
12
+ position?: number;
18
13
  }
19
- interface AppendToSubMenuAction {
20
- type: 'appendToSubMenu';
14
+ /**
15
+ * Add one item at `menuPath` — the top-level menu, then any sub-menus, which
16
+ * are created as needed. `position` appends when omitted and counts from the
17
+ * end when negative.
18
+ */
19
+ interface AddItemAction {
20
+ type: 'addItem';
21
21
  menuPath: string[];
22
22
  menuItem: MenuItem;
23
- }
24
- interface AppendMenuAction {
25
- type: 'appendMenu';
26
- menuName: string;
27
- }
28
- interface InsertMenuAction {
29
- type: 'insertMenu';
30
- menuName: string;
31
- position: number;
23
+ position?: number;
32
24
  }
33
25
  interface SetMenusAction {
34
26
  type: 'setMenus';
35
- newMenus: Menu[];
27
+ newMenus: MenuDefinition[];
28
+ }
29
+ /**
30
+ * `addMenu`/`setMenus` change which menus exist, so they resolve eagerly — the
31
+ * app bar renders one button per menu before anything is opened. `addItem` only
32
+ * changes a menu's contents, so it resolves when that menu opens, against
33
+ * whatever its items are at that moment.
34
+ */
35
+ export type MenuAction = AddMenuAction | AddItemAction | SetMenusAction;
36
+ /**
37
+ * A menu as a root model authors it. Items may be a plain array, or a thunk for
38
+ * a menu whose contents depend on state that would otherwise be read — and so
39
+ * tracked — every time the app bar renders.
40
+ */
41
+ export interface MenuDefinition {
42
+ label: string;
43
+ menuItems: MenuItemsGetter;
36
44
  }
37
- export type MenuAction = InsertMenuAction | AppendMenuAction | AppendToSubMenuAction | AppendToMenuAction | InsertInMenuAction | InsertInSubMenuAction | SetMenusAction;
45
+ /**
46
+ * A menu as the app bar consumes it. Always a thunk returning a fresh array: a
47
+ * menu's items are produced when it opens and never before, so no consumer has
48
+ * to care which form the root model authored, and a plugin's contributions are
49
+ * merged in at that point rather than spliced into the definition.
50
+ */
38
51
  export interface Menu {
39
52
  label: string;
40
- menuItems: MenuItem[];
53
+ menuItems: () => MenuItem[];
41
54
  }
42
- export declare function appendMenu({ menus, menuName, }: {
43
- menus: Menu[];
44
- menuName: string;
45
- }): number;
46
- export declare function insertMenu({ menus, menuName, position, }: {
47
- menus: Menu[];
48
- menuName: string;
49
- position: number;
50
- }): number;
51
- export declare function appendToMenu({ menus, menuName, menuItem, }: {
52
- menus: Menu[];
53
- menuName: string;
54
- menuItem: MenuItem;
55
- }): number;
56
- export declare function insertInMenu({ menus, menuName, menuItem, position, }: {
57
- menus: Menu[];
58
- menuName: string;
59
- menuItem: MenuItem;
60
- position: number;
61
- }): number;
62
- export declare function appendToSubMenu({ menus, menuPath, menuItem, }: {
63
- menus: Menu[];
64
- menuPath: string[];
65
- menuItem: MenuItem;
66
- }): number;
67
- export declare function insertInSubMenu({ menus, menuPath, menuItem, position, }: {
68
- menus: Menu[];
69
- menuPath: string[];
70
- menuItem: MenuItem;
71
- position: number;
72
- }): number;
73
- export declare function processMutableMenuActions(ret: Menu[], actions: MenuAction[]): Menu[];
55
+ /**
56
+ * Resolve a root model's menu definitions against the contributions plugins
57
+ * have pushed (via `RootAppMenuMixin`). Structural contributions apply now;
58
+ * item contributions are recorded against their target menu and applied when
59
+ * that menu opens, so a menu that computes its items lazily stays lazy and
60
+ * nothing is ever spliced into a definition.
61
+ *
62
+ * Runs on every `menus()` evaluation, so it must not mutate `base` or anything
63
+ * an action carries.
64
+ */
65
+ export declare function processMutableMenuActions(base: MenuDefinition[], actions: MenuAction[]): Menu[];
66
+ /**
67
+ * Flatten resolved menus to plain data by opening every one of them. For tests
68
+ * and other callers that want to assert on menu contents rather than render
69
+ * them.
70
+ */
71
+ export declare function resolveMenus(menus: Menu[]): {
72
+ label: string;
73
+ menuItems: MenuItem[];
74
+ }[];
74
75
  export {};
package/esm/menus.js CHANGED
@@ -1,99 +1,146 @@
1
- export function appendMenu({ menus, menuName, }) {
2
- return menus.push({ label: menuName, menuItems: [] });
1
+ // a contribution that threw is reported once rather than on every open — the
2
+ // menu re-resolves on each open and, while open, on each observer re-render
3
+ const reported = new WeakSet();
4
+ /**
5
+ * The action log is permanent; the menu it builds is thrown away when the menu
6
+ * closes. Everything a resolution walks therefore has to belong to the
7
+ * throwaway, because resolving a path is a **write**:
8
+ * `appendToSubMenu(['Add', 'My plugin'], two)` means "find the array labelled
9
+ * My plugin and push into it".
10
+ *
11
+ * So when an earlier action is what supplied that array —
12
+ * `appendToMenu('Add', { label: 'My plugin', subMenu: [one] })` — and it goes
13
+ * into the menu by reference, the push lands in the stored action instead. The
14
+ * next open replays a log that now reads `subMenu: [one, two]` and pushes
15
+ * again, so the sub-menu gains an item on every open, and on every observer
16
+ * re-render while open.
17
+ *
18
+ * Cloning is what keeps the two apart: the copy is what the path resolves to
19
+ * and what gets written, and it dies with the open. Leaf items are shared by
20
+ * reference — nothing writes into them — so this costs an allocation only for
21
+ * the rows that nest.
22
+ */
23
+ function cloneMenuItem(item) {
24
+ return 'subMenu' in item
25
+ ? { ...item, subMenu: cloneMenuItems(item.subMenu) }
26
+ : item;
3
27
  }
4
- export function insertMenu({ menus, menuName, position, }) {
5
- menus.splice((position < 0 ? menus.length : 0) + position, 0, {
6
- label: menuName,
7
- menuItems: [],
8
- });
9
- return menus.length;
28
+ function cloneMenuItems(items) {
29
+ return items.map(item => cloneMenuItem(item));
10
30
  }
11
- export function appendToMenu({ menus, menuName, menuItem, }) {
12
- const menu = menus.find(m => m.label === menuName);
13
- if (!menu) {
14
- menus.push({ label: menuName, menuItems: [menuItem] });
15
- return 1;
16
- }
17
- return menu.menuItems.push(menuItem);
31
+ function materialize(menuItems) {
32
+ return typeof menuItems === 'function' ? menuItems() : menuItems;
18
33
  }
19
- export function insertInMenu({ menus, menuName, menuItem, position, }) {
20
- const menu = menus.find(m => m.label === menuName);
21
- if (!menu) {
22
- menus.push({ label: menuName, menuItems: [menuItem] });
23
- return 1;
24
- }
25
- const insertPosition = position < 0 ? menu.menuItems.length + position : position;
26
- menu.menuItems.splice(insertPosition, 0, menuItem);
27
- return menu.menuItems.length;
34
+ // splice is already the "counts from the end when negative" rule, and clamps
35
+ // rather than wrapping past the start; the default is what makes it an append
36
+ function insertAt(items, item, position = items.length) {
37
+ items.splice(position, 0, item);
28
38
  }
29
- export function appendToSubMenu({ menus, menuPath, menuItem, }) {
30
- let topMenu = menus.find(m => m.label === menuPath[0]);
31
- if (!topMenu) {
32
- const idx = appendMenu({ menus, menuName: menuPath[0] });
33
- topMenu = menus[idx - 1];
34
- }
35
- let { menuItems: subMenu } = topMenu;
36
- const pathSoFar = [menuPath[0]];
37
- for (const menuName of menuPath.slice(1)) {
38
- pathSoFar.push(menuName);
39
- let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName);
40
- if (!sm) {
41
- const idx = subMenu.push({ label: menuName, subMenu: [] });
42
- sm = subMenu[idx - 1];
43
- }
44
- if (!('subMenu' in sm)) {
45
- throw new Error(`"${menuName}" in path "${pathSoFar}" is not a subMenu`);
39
+ /**
40
+ * Walk `menuPath` past its first segment (the top-level menu, already
41
+ * resolved), creating empty sub-menus as needed, and return the deepest
42
+ * sub-menu's item array. Throws if a path segment names an item that is not a
43
+ * sub-menu.
44
+ */
45
+ function resolveSubMenu(items, menuPath) {
46
+ let level = items;
47
+ for (const [idx, menuName] of menuPath.slice(1).entries()) {
48
+ let sub = level.find((mi) => 'subMenu' in mi && mi.label === menuName);
49
+ if (!sub) {
50
+ if (level.some(mi => 'label' in mi && mi.label === menuName)) {
51
+ const pathSoFar = menuPath.slice(0, idx + 2).join(' > ');
52
+ throw new Error(`"${menuName}" in path "${pathSoFar}" is not a subMenu`);
53
+ }
54
+ sub = { label: menuName, subMenu: [] };
55
+ level.push(sub);
46
56
  }
47
- subMenu = sm.subMenu;
57
+ level = sub.subMenu;
48
58
  }
49
- return subMenu.push(menuItem);
59
+ return level;
50
60
  }
51
- export function insertInSubMenu({ menus, menuPath, menuItem, position, }) {
52
- let topMenu = menus.find(m => m.label === menuPath[0]);
53
- if (!topMenu) {
54
- const idx = appendMenu({ menus, menuName: menuPath[0] });
55
- topMenu = menus[idx - 1];
56
- }
57
- let { menuItems: subMenu } = topMenu;
58
- const pathSoFar = [menuPath[0]];
59
- for (const menuName of menuPath.slice(1)) {
60
- pathSoFar.push(menuName);
61
- let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName);
62
- if (!sm) {
63
- const idx = subMenu.push({ label: menuName, subMenu: [] });
64
- sm = subMenu[idx - 1];
61
+ // each contribution is applied independently: a plugin that names a bad path
62
+ // loses its own item rather than every other plugin's, and never the session —
63
+ // this runs from the app bar's click handler, where a throw goes straight
64
+ // through React
65
+ function applyItemActions(items, actions) {
66
+ for (const action of actions) {
67
+ try {
68
+ const target = resolveSubMenu(items, action.menuPath);
69
+ // cloned rather than inserted by reference — see cloneMenuItem
70
+ insertAt(target, cloneMenuItem(action.menuItem), action.position);
65
71
  }
66
- if (!('subMenu' in sm)) {
67
- throw new Error(`"${menuName}" in path "${pathSoFar}" is not a subMenu`);
72
+ catch (error) {
73
+ if (!reported.has(action)) {
74
+ reported.add(action);
75
+ console.error(error);
76
+ }
68
77
  }
69
- subMenu = sm.subMenu;
70
78
  }
71
- subMenu.splice(position, 0, menuItem);
72
- return subMenu.length;
79
+ return items;
80
+ }
81
+ // first menu with this label, or a new empty one — an item action naming a menu
82
+ // that doesn't exist creates it, so a plugin can populate its own menu without
83
+ // declaring it first
84
+ function findOrCreateMenu(pending, label, position) {
85
+ const found = pending.find(m => m.label === label);
86
+ if (found) {
87
+ return found;
88
+ }
89
+ const menu = { label, base: [], itemActions: [] };
90
+ insertAt(pending, menu, position);
91
+ return menu;
73
92
  }
74
- export function processMutableMenuActions(ret, actions) {
93
+ function toMenu({ label, base, itemActions }) {
94
+ return {
95
+ label,
96
+ menuItems: () => applyItemActions(cloneMenuItems(materialize(base)), itemActions),
97
+ };
98
+ }
99
+ /**
100
+ * Resolve a root model's menu definitions against the contributions plugins
101
+ * have pushed (via `RootAppMenuMixin`). Structural contributions apply now;
102
+ * item contributions are recorded against their target menu and applied when
103
+ * that menu opens, so a menu that computes its items lazily stays lazy and
104
+ * nothing is ever spliced into a definition.
105
+ *
106
+ * Runs on every `menus()` evaluation, so it must not mutate `base` or anything
107
+ * an action carries.
108
+ */
109
+ export function processMutableMenuActions(base, actions) {
110
+ const toPending = (m) => ({
111
+ label: m.label,
112
+ base: m.menuItems,
113
+ itemActions: [],
114
+ });
115
+ let pending = base.map(toPending);
75
116
  for (const action of actions) {
76
- if (action.type === 'setMenus') {
77
- ret = action.newMenus;
78
- }
79
- else if (action.type === 'appendMenu') {
80
- appendMenu({ menus: ret, ...action });
81
- }
82
- else if (action.type === 'insertMenu') {
83
- insertMenu({ menus: ret, ...action });
84
- }
85
- else if (action.type === 'insertInSubMenu') {
86
- insertInSubMenu({ menus: ret, ...action });
87
- }
88
- else if (action.type === 'appendToSubMenu') {
89
- appendToSubMenu({ menus: ret, ...action });
90
- }
91
- else if (action.type === 'appendToMenu') {
92
- appendToMenu({ menus: ret, ...action });
93
- }
94
- else if (action.type === 'insertInMenu') {
95
- insertInMenu({ menus: ret, ...action });
117
+ switch (action.type) {
118
+ case 'setMenus': {
119
+ // replaces the menu bar wholesale, so item contributions made before it
120
+ // are dropped along with the menus they targeted
121
+ pending = action.newMenus.map(toPending);
122
+ break;
123
+ }
124
+ case 'addMenu': {
125
+ findOrCreateMenu(pending, action.menuName, action.position);
126
+ break;
127
+ }
128
+ case 'addItem': {
129
+ findOrCreateMenu(pending, action.menuPath[0]).itemActions.push(action);
130
+ break;
131
+ }
132
+ default: {
133
+ action;
134
+ }
96
135
  }
97
136
  }
98
- return ret;
137
+ return pending.map(m => toMenu(m));
138
+ }
139
+ /**
140
+ * Flatten resolved menus to plain data by opening every one of them. For tests
141
+ * and other callers that want to assert on menu contents rather than render
142
+ * them.
143
+ */
144
+ export function resolveMenus(menus) {
145
+ return menus.map(m => ({ label: m.label, menuItems: m.menuItems() }));
99
146
  }
@@ -0,0 +1,9 @@
1
+ export declare function readQueryParams<T extends string>(keys: T[]): Record<T, string | undefined>;
2
+ export declare function readAllQueryParams(): URLSearchParams;
3
+ export declare function deleteQueryParams(keys: readonly string[]): void;
4
+ export declare function setQueryParams(values: Record<string, string | undefined>): void;
5
+ /**
6
+ * Read one param and keep re-rendering as it changes, including across the
7
+ * `replaceState` writes above, which fire no event of their own.
8
+ */
9
+ export declare function useQueryParam(key: string): readonly [string | undefined, (newValue: string | undefined) => void];
@@ -0,0 +1,92 @@
1
+ import { useCallback, useSyncExternalStore } from 'react';
2
+ // The single decision of where this URL keeps its params. Computed once per
3
+ // operation and threaded through read+write so the two can't disagree.
4
+ function paramLocation() {
5
+ return window.location.hash.includes('=') ? 'hash' : 'search';
6
+ }
7
+ function readParams(loc) {
8
+ return new URLSearchParams(loc === 'hash' ? window.location.hash.slice(1) : window.location.search);
9
+ }
10
+ // history.replaceState fires no event, so a write has to notify readers itself.
11
+ // popstate alone covers only back/forward, which nothing here does: desktop's
12
+ // Loader clears its `?config=` with the very setter defined here, and with no
13
+ // notification it kept reading the stale value and stayed on the loading screen
14
+ // (the "fall back to the start screen" error path never arrived).
15
+ const listeners = new Set();
16
+ function subscribe(callback) {
17
+ listeners.add(callback);
18
+ window.addEventListener('popstate', callback);
19
+ return () => {
20
+ listeners.delete(callback);
21
+ window.removeEventListener('popstate', callback);
22
+ };
23
+ }
24
+ // Writes params back into `loc`, leaving the other URL component untouched — the
25
+ // URL hash/search setters each replace only their own component.
26
+ function writeParams(loc, params) {
27
+ const url = new URL(window.location.href);
28
+ const str = params.toString();
29
+ if (loc === 'hash') {
30
+ url.hash = str;
31
+ }
32
+ else {
33
+ url.search = str;
34
+ }
35
+ window.history.replaceState(null, '', url.href);
36
+ // Copied, which this note has claimed while the loop read the live set. A
37
+ // listener is React's store-change callback, and React may flush the re-render
38
+ // synchronously inside it, so the set can be mutated mid-notify: an unmounted
39
+ // reader unsubscribes (safe either way) and a remounted one re-adds itself at
40
+ // the end, where iterating the live set would visit it again for a change it
41
+ // has already read. The snapshot makes who gets notified for one change fixed
42
+ // before the first call.
43
+ for (const listener of [...listeners]) {
44
+ listener();
45
+ }
46
+ }
47
+ export function readQueryParams(keys) {
48
+ const params = readParams(paramLocation());
49
+ const result = {};
50
+ for (const key of keys) {
51
+ result[key] = params.get(key) ?? undefined;
52
+ }
53
+ return result;
54
+ }
55
+ // Reads the full current param set from wherever this URL keeps them (hash XOR
56
+ // search). Producers that need every param at once (buildShareUrl) share this
57
+ // so the hash/search decision stays in this one module.
58
+ export function readAllQueryParams() {
59
+ return readParams(paramLocation());
60
+ }
61
+ export function deleteQueryParams(keys) {
62
+ const loc = paramLocation();
63
+ const params = readParams(loc);
64
+ for (const key of keys) {
65
+ params.delete(key);
66
+ }
67
+ writeParams(loc, params);
68
+ }
69
+ export function setQueryParams(values) {
70
+ const loc = paramLocation();
71
+ const params = readParams(loc);
72
+ for (const [key, value] of Object.entries(values)) {
73
+ if (value === undefined) {
74
+ params.delete(key);
75
+ }
76
+ else {
77
+ params.set(key, value);
78
+ }
79
+ }
80
+ writeParams(loc, params);
81
+ }
82
+ /**
83
+ * Read one param and keep re-rendering as it changes, including across the
84
+ * `replaceState` writes above, which fire no event of their own.
85
+ */
86
+ export function useQueryParam(key) {
87
+ const value = useSyncExternalStore(subscribe, () => readParams(paramLocation()).get(key), () => null);
88
+ const setValue = useCallback((newValue) => {
89
+ setQueryParams({ [key]: newValue });
90
+ }, [key]);
91
+ return [value ?? undefined, setValue];
92
+ }
@@ -0,0 +1,18 @@
1
+ import type PluginManager from '@jbrowse/core/PluginManager';
2
+ import type { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager/assemblyConfigSchema';
3
+ import type { IAnyType } from '@jbrowse/mobx-state-tree';
4
+ /**
5
+ * What each app product's root model takes as its `sessionModelFactory`
6
+ * argument: the root builds the assembly config schema once and hands it to the
7
+ * session model, so the session's assembly slots are the same type the root's
8
+ * are rather than a second schema built from the same plugins.
9
+ *
10
+ * One name for the three roots that take it (web, desktop, react-app). They had
11
+ * a copy each, and two of them spelled the schema as a local
12
+ * `ReturnType<typeof assemblyConfigSchemaFactory>` alias — which is what
13
+ * `BaseAssemblyConfigSchema` already is.
14
+ */
15
+ export type SessionModelFactory = (args: {
16
+ pluginManager: PluginManager;
17
+ assemblyConfigSchema: BaseAssemblyConfigSchema;
18
+ }) => IAnyType;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,18 +1,7 @@
1
- import type { MenuItem as JBMenuItem } from '@jbrowse/core/ui';
2
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
3
- import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
4
- interface Menu {
5
- label: string;
6
- menuItems: JBMenuItem[];
7
- }
1
+ import type { AppSession } from './types.ts';
8
2
  interface Props {
9
3
  HeaderButtons?: React.ReactElement;
10
- session: SessionWithFocusedViewAndDrawerWidgets & {
11
- menus: () => Menu[];
12
- snackbarMessages: SnackbarMessage[];
13
- renameCurrentSession: (arg: string) => void;
14
- popSnackbarMessage: () => unknown;
15
- };
4
+ session: AppSession;
16
5
  }
17
- declare const App: (props: Props) => import("react/jsx-runtime").JSX.Element;
6
+ declare const App: (props: Props) => import("react").JSX.Element;
18
7
  export { App };
package/esm/ui/App/App.js CHANGED
@@ -2,42 +2,55 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Suspense, lazy } from 'react';
3
3
  import Snackbar from '@jbrowse/core/ui/Snackbar';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
+ import { ModalWidget, drawerGridTemplateColumns } from '@jbrowse/product-core';
5
6
  import { AppBar } from '@mui/material';
6
7
  import { observer } from 'mobx-react';
7
- import AppFab from "./AppFab.js";
8
- import AppToolbar from "./AppToolbar.js";
9
- import DialogQueue from "./DialogQueue.js";
10
- import ViewsContainer from "./ViewsContainer.js";
11
- const DrawerWidget = lazy(() => import("./DrawerWidget.js"));
8
+ import AppFab from './AppFab.js';
9
+ import AppReadyMarker from './AppReadyMarker.js';
10
+ import AppToolbar from './AppToolbar.js';
11
+ import DialogQueue from './DialogQueue.js';
12
+ import ViewsContainer from './ViewsContainer.js';
13
+ import { lazyChunk } from './lazyChunk.js';
14
+ // lazies
15
+ const DrawerWidget = lazy(lazyChunk('DrawerWidget', () => import('./DrawerWidget.js')));
12
16
  const useStyles = makeStyles()(theme => ({
13
17
  root: {
14
18
  display: 'grid',
15
- height: '100vh',
19
+ // Embedders can fit the app to its container by setting the
20
+ // --jbrowse-app-height CSS variable (e.g. to 100%); it defaults to the
21
+ // full viewport for standalone/full-window use.
22
+ height: 'var(--jbrowse-app-height, 100vh)',
23
+ // the containing block for AppFab, which floats over the app rather than
24
+ // over the page
25
+ position: 'relative',
26
+ // pin the single implicit row to the container height so appContainer
27
+ // fills it (an auto row would instead grow to content and overflow)
28
+ gridTemplateRows: 'minmax(0, 1fr)',
16
29
  width: '100%',
17
30
  colorScheme: theme.palette.mode,
18
31
  },
19
32
  appContainer: {
20
33
  gridColumn: 'main',
21
34
  display: 'grid',
22
- gridTemplateRows: '[menubar] min-content [components] auto',
23
- height: '100vh',
35
+ gridTemplateRows: '[menubar] min-content [components] minmax(0, 1fr)',
36
+ height: '100%',
24
37
  },
25
38
  appBar: {
26
39
  flexGrow: 1,
27
40
  gridRow: 'menubar',
28
41
  },
29
42
  }));
30
- const LazyDrawerWidget = observer(function LazyDrawerWidget(props) {
31
- const { session } = props;
32
- return (_jsx(Suspense, { fallback: null, children: _jsx(DrawerWidget, { session: session }) }));
33
- });
34
43
  const App = observer(function App(props) {
35
44
  const { session } = props;
36
45
  const { classes } = useStyles();
37
- const { minimized, visibleWidget, drawerWidth, drawerPosition } = session;
38
- const drawerVisible = visibleWidget && !minimized;
39
- const d = drawerVisible ? `[drawer] ${drawerWidth}px` : undefined;
40
- const grid = drawerPosition === 'right' ? ['[main] 1fr', d] : [d, '[main] 1fr'];
41
- return (_jsxs("div", { className: classes.root, style: { gridTemplateColumns: grid.filter(f => !!f).join(' ') }, children: [drawerVisible && drawerPosition === 'left' ? (_jsx(LazyDrawerWidget, { session: session })) : null, _jsx(DialogQueue, { session: session }), _jsxs("div", { className: classes.appContainer, children: [_jsx(AppBar, { className: classes.appBar, position: "static", children: _jsx(AppToolbar, { ...props }) }), _jsx(ViewsContainer, { ...props })] }), _jsx(AppFab, { session: session }), drawerVisible && drawerPosition === 'right' ? (_jsx(LazyDrawerWidget, { session: session })) : null, _jsx(Snackbar, { session: session })] }));
46
+ const { drawerVisible, drawerWidth, drawerPosition, poppedOut } = session;
47
+ const gridTemplateColumns = drawerGridTemplateColumns({
48
+ drawerVisible,
49
+ drawerPosition,
50
+ drawerWidth,
51
+ });
52
+ return (_jsxs("div", { className: classes.root, style: { gridTemplateColumns }, children: [poppedOut ? (_jsx(ModalWidget, { session: session, onClose: () => {
53
+ session.returnWidgetToDrawer();
54
+ } })) : null, _jsx(DialogQueue, { session: session }), _jsxs("div", { className: classes.appContainer, children: [_jsx(AppBar, { className: classes.appBar, position: "static", "data-testid": "app-bar", children: _jsx(AppToolbar, { ...props }) }), _jsx(ViewsContainer, { ...props })] }), _jsx(AppFab, { session: session }), _jsx(AppReadyMarker, { session: session }), drawerVisible ? (_jsx(Suspense, { fallback: null, children: _jsx(DrawerWidget, { session: session }) })) : null, _jsx(Snackbar, { session: session })] }));
42
55
  });
43
56
  export { App };
@@ -1,5 +1,14 @@
1
- import type { SessionWithDrawerWidgets } from '@jbrowse/core/util';
1
+ import type { DrawerPosition } from '@jbrowse/core/util';
2
+ /** All the FAB needs of a session, narrowed the way `DrawerChromeSession` is. */
3
+ export interface AppFabSession {
4
+ minimized: boolean;
5
+ activeWidgets: {
6
+ size: number;
7
+ };
8
+ drawerPosition: DrawerPosition;
9
+ showWidgetDrawer: () => void;
10
+ }
2
11
  declare const AppFab: ({ session, }: {
3
- session: SessionWithDrawerWidgets;
4
- }) => import("react/jsx-runtime").JSX.Element | null;
12
+ session: AppFabSession;
13
+ }) => import("react").JSX.Element | null;
5
14
  export default AppFab;