@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
package/esm/index.js CHANGED
@@ -1,9 +1,16 @@
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, DialogQueue, preloadAppFrame, preloadAppShell, } 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';
14
+ // The helper library an agent drives the app through: desktop serves it to
15
+ // run_javascript over MCP, web hands it to the page as window.jb
16
+ export { createJbApi, ensureReExports, safeJson, sessionOf, undeliveredNotifications, waitReady, } from './JbApi/index.js';
package/esm/menus.d.ts CHANGED
@@ -1,74 +1,61 @@
1
- import type { MenuItem } from '@jbrowse/core/ui';
2
- interface InsertInSubMenuAction {
3
- type: 'insertInSubMenu';
1
+ import type { MenuItem } from '@jbrowse/core/ui/menuItems';
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';
11
+ menuName: string;
12
+ position?: number;
13
+ }
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';
4
21
  menuPath: string[];
5
22
  menuItem: MenuItem;
6
- position: number;
7
- }
8
- interface InsertInMenuAction {
9
- type: 'insertInMenu';
10
- menuName: string;
11
- menuItem: MenuItem;
12
- position: number;
13
- }
14
- interface AppendToMenuAction {
15
- type: 'appendToMenu';
16
- menuName: string;
17
- menuItem: MenuItem;
18
- }
19
- interface AppendToSubMenuAction {
20
- type: 'appendToSubMenu';
21
- menuPath: string[];
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;
32
- }
33
- interface SetMenusAction {
34
- type: 'setMenus';
35
- newMenus: Menu[];
36
- }
37
- export type MenuAction = InsertMenuAction | AppendMenuAction | AppendToSubMenuAction | AppendToMenuAction | InsertInMenuAction | InsertInSubMenuAction | SetMenusAction;
23
+ position?: number;
24
+ }
25
+ /**
26
+ * `addMenu` changes which menus exist, so it resolves eagerly — the app bar
27
+ * renders one button per menu before anything is opened. `addItem` only changes
28
+ * a menu's contents, so it resolves when that menu opens, against whatever its
29
+ * items are at that moment.
30
+ */
31
+ export type MenuAction = AddMenuAction | AddItemAction;
32
+ /**
33
+ * A top-level menu. `menuItems` is a thunk the app bar calls when the menu
34
+ * opens and never before, so a root model's reads stay out of the bar's
35
+ * tracking scope and a plugin's contributions are merged onto each fresh
36
+ * result rather than spliced into a definition.
37
+ */
38
38
  export interface Menu {
39
+ label: string;
40
+ menuItems: () => MenuItem[];
41
+ }
42
+ /**
43
+ * Resolve a root model's menus against the contributions plugins have pushed
44
+ * (via `RootAppMenuMixin`). Structural contributions apply now; item
45
+ * contributions are grouped by path and merged when the menu they target
46
+ * opens.
47
+ *
48
+ * Runs on every `menus()` evaluation, so it must not mutate `base` or anything
49
+ * an action carries.
50
+ */
51
+ export declare function processMutableMenuActions(base: Menu[], actions: MenuAction[]): Menu[];
52
+ /**
53
+ * Flatten resolved menus to plain data by opening every one of them. For tests
54
+ * and other callers that want to assert on menu contents rather than render
55
+ * them.
56
+ */
57
+ export declare function resolveMenus(menus: Menu[]): {
39
58
  label: string;
40
59
  menuItems: MenuItem[];
41
- }
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[];
60
+ }[];
74
61
  export {};
package/esm/menus.js CHANGED
@@ -1,99 +1,130 @@
1
- export function appendMenu({ menus, menuName, }) {
2
- return menus.push({ label: menuName, menuItems: [] });
3
- }
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;
10
- }
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;
1
+ // a conflict is reported once rather than on every open — the menu re-resolves
2
+ // on each open and, while open, on each observer re-render — and the action is
3
+ // what outlives the nodes, which are rebuilt on every menus() evaluation
4
+ const reported = new WeakSet();
5
+ function register(node, action, depth) {
6
+ const label = action.menuPath[depth];
7
+ if (label === undefined) {
8
+ node.entries.push(action);
16
9
  }
17
- return menu.menuItems.push(menuItem);
18
- }
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;
10
+ else {
11
+ let entry = node.entries.find((e) => e.type === 'subMenu' && e.label === label);
12
+ if (!entry) {
13
+ entry = {
14
+ type: 'subMenu',
15
+ label,
16
+ path: action.menuPath.slice(0, depth + 1),
17
+ node: { entries: [] },
18
+ first: action,
19
+ };
20
+ node.entries.push(entry);
21
+ }
22
+ register(entry.node, action, depth + 1);
24
23
  }
25
- const insertPosition = position < 0 ? menu.menuItems.length + position : position;
26
- menu.menuItems.splice(insertPosition, 0, menuItem);
27
- return menu.menuItems.length;
28
24
  }
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];
25
+ // splice is already the "counts from the end when negative" rule, and clamps
26
+ // rather than wrapping past the start; the default is what makes it an append
27
+ function insertAt(items, item, position = items.length) {
28
+ items.splice(position, 0, item);
29
+ }
30
+ // the author's form is kept: an array sub-menu resolves now, a thunk sub-menu
31
+ // resolves when its panel opens, with the contributions merged in either way
32
+ function withContributions(item, node) {
33
+ const { subMenu } = item;
34
+ return {
35
+ ...item,
36
+ subMenu: typeof subMenu === 'function'
37
+ ? () => apply(subMenu(), node)
38
+ : apply(subMenu, node),
39
+ };
40
+ }
41
+ /**
42
+ * Return a copy of `items` with a node's contributions merged in. Nothing is
43
+ * written into `items`, into any sub-menu it holds, or into a contribution's
44
+ * own payload: a sub-menu that gains contributions is replaced by a copy whose
45
+ * `subMenu` resolves them, so the arrays behind a definition or an action are
46
+ * the same on every open.
47
+ *
48
+ * A path segment naming a row that is not a sub-menu is a plugin bug. It
49
+ * costs that subtree of contributions, reported once, and nothing else — this
50
+ * runs from the app bar's click handler, where a throw goes straight through
51
+ * React.
52
+ */
53
+ function apply(items, node) {
54
+ const result = [...items];
55
+ for (const entry of node.entries) {
56
+ if (entry.type === 'addItem') {
57
+ insertAt(result, entry.menuItem, entry.position);
43
58
  }
44
- if (!('subMenu' in sm)) {
45
- throw new Error(`"${menuName}" in path "${pathSoFar}" is not a subMenu`);
59
+ else {
60
+ const idx = result.findIndex(mi => 'label' in mi && mi.label === entry.label);
61
+ const found = result[idx];
62
+ if (found === undefined) {
63
+ result.push({ label: entry.label, subMenu: apply([], entry.node) });
64
+ }
65
+ else if ('subMenu' in found) {
66
+ result[idx] = withContributions(found, entry.node);
67
+ }
68
+ else if (!reported.has(entry.first)) {
69
+ reported.add(entry.first);
70
+ console.error(new Error(`"${entry.label}" in path "${entry.path.join(' > ')}" is not a subMenu`));
71
+ }
46
72
  }
47
- subMenu = sm.subMenu;
48
73
  }
49
- return subMenu.push(menuItem);
74
+ return result;
50
75
  }
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];
76
+ // first menu with this label, or a new empty one — an item action naming a menu
77
+ // that doesn't exist creates it, so a plugin can populate its own menu without
78
+ // declaring it first
79
+ function findOrCreateMenu(pending, label, position) {
80
+ const found = pending.find(m => m.label === label);
81
+ if (found) {
82
+ return found;
56
83
  }
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];
65
- }
66
- if (!('subMenu' in sm)) {
67
- throw new Error(`"${menuName}" in path "${pathSoFar}" is not a subMenu`);
68
- }
69
- subMenu = sm.subMenu;
70
- }
71
- subMenu.splice(position, 0, menuItem);
72
- return subMenu.length;
84
+ const menu = {
85
+ label,
86
+ menuItems: () => [],
87
+ node: { entries: [] },
88
+ };
89
+ insertAt(pending, menu, position);
90
+ return menu;
73
91
  }
74
- export function processMutableMenuActions(ret, actions) {
92
+ /**
93
+ * Resolve a root model's menus against the contributions plugins have pushed
94
+ * (via `RootAppMenuMixin`). Structural contributions apply now; item
95
+ * contributions are grouped by path and merged when the menu they target
96
+ * opens.
97
+ *
98
+ * Runs on every `menus()` evaluation, so it must not mutate `base` or anything
99
+ * an action carries.
100
+ */
101
+ export function processMutableMenuActions(base, actions) {
102
+ const pending = base.map(m => ({ ...m, node: { entries: [] } }));
75
103
  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 });
104
+ switch (action.type) {
105
+ case 'addMenu': {
106
+ findOrCreateMenu(pending, action.menuName, action.position);
107
+ break;
108
+ }
109
+ case 'addItem': {
110
+ register(findOrCreateMenu(pending, action.menuPath[0]).node, action, 1);
111
+ break;
112
+ }
113
+ default: {
114
+ action;
115
+ }
96
116
  }
97
117
  }
98
- return ret;
118
+ return pending.map(({ label, menuItems, node }) => ({
119
+ label,
120
+ menuItems: () => apply(menuItems(), node),
121
+ }));
122
+ }
123
+ /**
124
+ * Flatten resolved menus to plain data by opening every one of them. For tests
125
+ * and other callers that want to assert on menu contents rather than render
126
+ * them.
127
+ */
128
+ export function resolveMenus(menus) {
129
+ return menus.map(m => ({ label: m.label, menuItems: m.menuItems() }));
99
130
  }
@@ -0,0 +1,9 @@
1
+ export declare function readQueryParams<T extends string>(keys: T[]): Record<T, string | undefined>;
2
+ export declare function readAllQueryParams(url?: URL | Location): 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,93 @@
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(url = window.location) {
5
+ return url.hash.includes('=') ? 'hash' : 'search';
6
+ }
7
+ function readParams(loc, url = window.location) {
8
+ return new URLSearchParams(loc === 'hash' ? url.hash.slice(1) : url.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 param set from wherever a URL keeps them (hash XOR search),
56
+ // the page's own by default. Producers that need every param at once
57
+ // (buildShareUrl) share this so the hash/search decision stays in this one
58
+ // module.
59
+ export function readAllQueryParams(url = window.location) {
60
+ return readParams(paramLocation(url), url);
61
+ }
62
+ export function deleteQueryParams(keys) {
63
+ const loc = paramLocation();
64
+ const params = readParams(loc);
65
+ for (const key of keys) {
66
+ params.delete(key);
67
+ }
68
+ writeParams(loc, params);
69
+ }
70
+ export function setQueryParams(values) {
71
+ const loc = paramLocation();
72
+ const params = readParams(loc);
73
+ for (const [key, value] of Object.entries(values)) {
74
+ if (value === undefined) {
75
+ params.delete(key);
76
+ }
77
+ else {
78
+ params.set(key, value);
79
+ }
80
+ }
81
+ writeParams(loc, params);
82
+ }
83
+ /**
84
+ * Read one param and keep re-rendering as it changes, including across the
85
+ * `replaceState` writes above, which fire no event of their own.
86
+ */
87
+ export function useQueryParam(key) {
88
+ const value = useSyncExternalStore(subscribe, () => readParams(paramLocation()).get(key), () => null);
89
+ const setValue = useCallback((newValue) => {
90
+ setQueryParams({ [key]: newValue });
91
+ }, [key]);
92
+ return [value ?? undefined, setValue];
93
+ }
@@ -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
@@ -1,43 +1,63 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, lazy } from 'react';
2
+ import { Suspense, useEffect } from 'react';
3
3
  import Snackbar from '@jbrowse/core/ui/Snackbar';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { AppBar } from '@mui/material';
5
+ import { AppReadyMarker, ModalWidget, drawerGridTemplateColumns, } from '@jbrowse/product-core';
6
+ import { AppBar, useMediaQuery, useTheme } 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 AppToolbar from './AppToolbar.js';
10
+ import DialogQueue from './DialogQueue.js';
11
+ import ViewsContainer from './ViewsContainer.js';
12
+ import { DrawerWidget } from './lazyParts.js';
12
13
  const useStyles = makeStyles()(theme => ({
13
14
  root: {
14
15
  display: 'grid',
15
- height: '100vh',
16
+ // Embedders can fit the app to its container by setting the
17
+ // --jbrowse-app-height CSS variable (e.g. to 100%); it defaults to the
18
+ // full viewport for standalone/full-window use.
19
+ height: 'var(--jbrowse-app-height, 100vh)',
20
+ // the containing block for AppFab, which floats over the app rather than
21
+ // over the page
22
+ position: 'relative',
23
+ // pin the single implicit row to the container height so appContainer
24
+ // fills it (an auto row would instead grow to content and overflow)
25
+ gridTemplateRows: 'minmax(0, 1fr)',
16
26
  width: '100%',
17
27
  colorScheme: theme.palette.mode,
18
28
  },
19
29
  appContainer: {
20
30
  gridColumn: 'main',
21
31
  display: 'grid',
22
- gridTemplateRows: '[menubar] min-content [components] auto',
23
- height: '100vh',
32
+ gridTemplateRows: '[menubar] min-content [components] minmax(0, 1fr)',
33
+ // an implicit column floors at the toolbar's min-content, which widened
34
+ // the page past a phone's screen rather than ellipsizing the session name
35
+ gridTemplateColumns: '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, modalWidgetVisible } = session;
47
+ // a phone has no room for a drawer column beside the views
48
+ const narrow = useMediaQuery(useTheme().breakpoints.down('sm'), {
49
+ noSsr: true,
50
+ });
51
+ useEffect(() => {
52
+ session.setModalWidgets(narrow);
53
+ }, [session, narrow]);
54
+ const gridTemplateColumns = drawerGridTemplateColumns({
55
+ drawerVisible,
56
+ drawerPosition,
57
+ drawerWidth,
58
+ });
59
+ return (_jsxs("div", { className: classes.root, style: { gridTemplateColumns }, children: [modalWidgetVisible ? (_jsx(ModalWidget, { session: session, fullScreen: narrow, onClose: () => {
60
+ session.closeModalWidget();
61
+ } })) : 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, { session: session })] }), _jsx(AppFab, { session: session }), _jsx(AppReadyMarker, { session: session }), drawerVisible ? (_jsx(Suspense, { fallback: null, children: _jsx(DrawerWidget, { session: session }) })) : null, _jsx(Snackbar, { session: session })] }));
42
62
  });
43
63
  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;