@jbrowse/app-core 5.0.0-beta.13 → 5.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/AppSession/AppSessionMixin.d.ts +110 -92
- package/esm/Assemblies/AssembliesMixin.d.ts +30 -0
- package/esm/HistoryManagement/index.d.ts +2 -0
- package/esm/HistoryManagement/index.js +2 -0
- package/esm/JBrowseConfig/RootConfiguration.d.ts +3 -2
- package/esm/JBrowseConfig/RootConfiguration.js +3 -2
- package/esm/JBrowseConfig/index.d.ts +2 -3
- package/esm/JBrowseConfig/index.js +2 -3
- package/esm/JBrowseModel/index.d.ts +9 -1
- package/esm/JBrowseModel/index.js +8 -7
- package/esm/JbApi/jbApi.js +5 -2
- package/esm/RootMenu/index.d.ts +2 -0
- package/esm/RootMenu/index.js +2 -0
- package/esm/SessionSpec/index.d.ts +2 -2
- package/esm/SessionSpec/index.js +1 -1
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +19 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +29 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.js +2 -10
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +1 -1
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +2 -3
- package/esm/WorkspaceLayout/WorkspaceTab.js +2 -2
- package/esm/WorkspaceLayout/model.d.ts +10 -15
- package/esm/WorkspaceLayout/model.js +36 -65
- package/esm/WorkspaceLayout/tree.d.ts +3 -6
- package/esm/WorkspaceLayout/tree.js +8 -12
- package/esm/WorkspaceLayout/useLayoutDrag.js +10 -4
- package/esm/index.d.ts +5 -3
- package/esm/index.js +3 -2
- package/esm/queryParams.d.ts +2 -0
- package/esm/queryParams.js +16 -5
- package/esm/sessionSummary.d.ts +16 -0
- package/esm/sessionSummary.js +57 -0
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +3 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +4 -2
- package/esm/ui/App/JBrowseTabMenu.d.ts +3 -2
- package/esm/ui/App/JBrowseTabMenu.js +4 -2
- package/esm/ui/App/ThemeModeButton.d.ts +4 -5
- package/esm/ui/App/ThemeModeButton.js +18 -11
- package/esm/ui/App/ViewContainer.js +7 -1
- package/esm/ui/App/ViewMenu.js +34 -25
- package/esm/ui/App/ViewWrapper.js +2 -2
- package/esm/ui/App/viewTitle.d.ts +6 -1
- package/esm/ui/App/viewTitle.js +7 -2
- package/package.json +6 -6
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import { createElementId } from '@jbrowse/core/util/types/mst';
|
|
2
|
-
import { cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
import { addDisposer, cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
|
|
3
|
+
import { autorun } from 'mobx';
|
|
3
4
|
import { resolveLayoutSpec, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
|
|
4
|
-
import {
|
|
5
|
+
import { addTab, addViewToTab, findTab, homeViews, moveTabToPanel, normalize, panelContainingView, panels, pruneEmptyPanel, pruneEmptyTab, removePanel, removeTab, removeView, renameTab, setActiveTab, setSizes, splitPanel, tabContainingView, tabs, } from './tree.js';
|
|
5
6
|
function sessionViewIds(self) {
|
|
6
7
|
return self.views?.map(v => v.id);
|
|
7
8
|
}
|
|
8
|
-
/**
|
|
9
|
-
* A caller's view list narrowed to the session's views, deduplicated. The list
|
|
10
|
-
* may be stale, and `applyLayoutSpec` throws on an unknown or repeated id.
|
|
11
|
-
*/
|
|
12
|
-
function liveViewIds(self, allViewIds) {
|
|
13
|
-
const live = sessionViewIds(self);
|
|
14
|
-
return [
|
|
15
|
-
...new Set(live ? allViewIds.filter(id => live.includes(id)) : allViewIds),
|
|
16
|
-
];
|
|
17
|
-
}
|
|
18
9
|
/**
|
|
19
10
|
* #stateModel WorkspaceLayoutMixin
|
|
20
11
|
*
|
|
@@ -104,16 +95,6 @@ export function WorkspaceLayoutMixin() {
|
|
|
104
95
|
panelContainingView(viewId) {
|
|
105
96
|
return panelContainingView(self.tree, viewId);
|
|
106
97
|
},
|
|
107
|
-
/** The views a tab renders: its members, in `session.views` order. */
|
|
108
|
-
viewIdsForTab(tabId, order) {
|
|
109
|
-
const members = new Set(findTab(self.tree, tabId)?.tab.viewIds ?? []);
|
|
110
|
-
return order.filter(id => members.has(id));
|
|
111
|
-
},
|
|
112
|
-
/** The tab a panel is showing, or its first. */
|
|
113
|
-
activeTabOf(panelId) {
|
|
114
|
-
const panel = panels(self.tree).find(p => p.id === panelId);
|
|
115
|
-
return panel ? activeTabIn(panel) : undefined;
|
|
116
|
-
},
|
|
117
98
|
/**
|
|
118
99
|
* The maximized cell alone, or the whole tree. The cell is resized to 1
|
|
119
100
|
* because CSS hands out free space by grow factor only up to a total of
|
|
@@ -161,33 +142,21 @@ export function WorkspaceLayoutMixin() {
|
|
|
161
142
|
function home(tree, viewIds) {
|
|
162
143
|
return homeViews(tree, viewIds, self.activePanelId, () => nextId('tab'));
|
|
163
144
|
}
|
|
164
|
-
function everyViewId(action, allViewIds) {
|
|
165
|
-
const ids = allViewIds ?? sessionViewIds(self);
|
|
166
|
-
if (!ids) {
|
|
167
|
-
throw new Error(`${action}(viewId, allViewIds): allViewIds is every view id in the session, and this session has no view list to default it from`);
|
|
168
|
-
}
|
|
169
|
-
return ids;
|
|
170
|
-
}
|
|
171
145
|
/**
|
|
172
|
-
* Homes every view first
|
|
173
|
-
*
|
|
174
|
-
* the
|
|
146
|
+
* Homes every view first: the homing autorun waits for the outermost
|
|
147
|
+
* action, so a view added in the same action has no tab yet. Returns
|
|
148
|
+
* where the view came from, or `undefined` if it is not in the
|
|
149
|
+
* session, in which case nothing is applied.
|
|
175
150
|
*/
|
|
176
|
-
function rehomeView(viewId,
|
|
177
|
-
const
|
|
151
|
+
function rehomeView(viewId, place) {
|
|
152
|
+
const ids = sessionViewIds(self);
|
|
153
|
+
const tree = ids ? home(self.tree, ids) : self.tree;
|
|
178
154
|
const from = tabContainingView(tree, viewId);
|
|
179
155
|
if (!from) {
|
|
180
156
|
return undefined;
|
|
181
157
|
}
|
|
182
158
|
const placed = place(removeView(tree, viewId), from);
|
|
183
|
-
|
|
184
|
-
const source = panels(pruned).find(p => p.id === from.panel.id);
|
|
185
|
-
const emptiedCell = source?.tabs.length === 1 &&
|
|
186
|
-
source.tabs[0].id === from.tab.id &&
|
|
187
|
-
source.tabs[0].viewIds.length === 0;
|
|
188
|
-
apply(emptiedCell
|
|
189
|
-
? pruneEmptyPanel(removeTab(pruned, from.tab.id), from.panel.id)
|
|
190
|
-
: pruned);
|
|
159
|
+
apply(pruneEmptyPanel(pruneEmptyTab(placed, from.tab.id), from.panel.id));
|
|
191
160
|
return from;
|
|
192
161
|
}
|
|
193
162
|
/**
|
|
@@ -329,12 +298,10 @@ export function WorkspaceLayoutMixin() {
|
|
|
329
298
|
},
|
|
330
299
|
/**
|
|
331
300
|
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
332
|
-
* `allViewIds` is EVERY view in the session, defaulting to the host's
|
|
333
|
-
* own list, since homing drops any view the list omits.
|
|
334
301
|
*/
|
|
335
|
-
moveViewToNewTab(viewId
|
|
302
|
+
moveViewToNewTab(viewId) {
|
|
336
303
|
const tab = { id: nextId('tab'), viewIds: [viewId] };
|
|
337
|
-
const from = rehomeView(viewId,
|
|
304
|
+
const from = rehomeView(viewId, (tree, at) => addTab(tree, at.panel.id, tab));
|
|
338
305
|
if (!from) {
|
|
339
306
|
return undefined;
|
|
340
307
|
}
|
|
@@ -345,7 +312,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
345
312
|
* ViewMenu's "move to split": the view leaves for a new cell to the
|
|
346
313
|
* right (`row`) or below (`column`) of its own.
|
|
347
314
|
*/
|
|
348
|
-
moveViewToSplit(viewId, direction
|
|
315
|
+
moveViewToSplit(viewId, direction) {
|
|
349
316
|
const tabId = nextId('tab');
|
|
350
317
|
const panel = {
|
|
351
318
|
id: nextId('panel'),
|
|
@@ -353,7 +320,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
353
320
|
tabs: [{ id: tabId, viewIds: [viewId] }],
|
|
354
321
|
activeTabId: tabId,
|
|
355
322
|
};
|
|
356
|
-
const from = rehomeView(viewId,
|
|
323
|
+
const from = rehomeView(viewId, (tree, at) => splitPanel(tree, at.panel.id, direction, panel));
|
|
357
324
|
if (!from) {
|
|
358
325
|
return undefined;
|
|
359
326
|
}
|
|
@@ -364,12 +331,12 @@ export function WorkspaceLayoutMixin() {
|
|
|
364
331
|
* ViewMenu's "move to tab": the view joins an existing tab, which
|
|
365
332
|
* becomes the one shown.
|
|
366
333
|
*/
|
|
367
|
-
moveViewToTab(viewId, tabId
|
|
334
|
+
moveViewToTab(viewId, tabId) {
|
|
368
335
|
const target = findTab(self.tree, tabId);
|
|
369
336
|
if (!target || target.tab.viewIds.includes(viewId)) {
|
|
370
337
|
return;
|
|
371
338
|
}
|
|
372
|
-
const from = rehomeView(viewId,
|
|
339
|
+
const from = rehomeView(viewId, tree => addViewToTab(tree, tabId, viewId));
|
|
373
340
|
const landed = findTab(self.tree, tabId);
|
|
374
341
|
if (from && landed) {
|
|
375
342
|
apply(setActiveTab(self.tree, landed.panel.id, tabId));
|
|
@@ -387,12 +354,11 @@ export function WorkspaceLayoutMixin() {
|
|
|
387
354
|
/**
|
|
388
355
|
* Move one view to a new tab or a split beside its cell, keeping the
|
|
389
356
|
* rest of the arrangement. PUBLIC API: protein3d and msaview call it
|
|
390
|
-
* with the move alone
|
|
391
|
-
*
|
|
357
|
+
* with the move alone. The second argument is an old plugin signature;
|
|
358
|
+
* the session's own views are read instead.
|
|
392
359
|
*/
|
|
393
|
-
setPendingMove(move,
|
|
394
|
-
|
|
395
|
-
const ids = sessionViewIds(self) ?? allViewIds;
|
|
360
|
+
setPendingMove(move, _allViewIds) {
|
|
361
|
+
const ids = sessionViewIds(self);
|
|
396
362
|
if (!move || !ids?.includes(move.viewId)) {
|
|
397
363
|
return;
|
|
398
364
|
}
|
|
@@ -400,23 +366,18 @@ export function WorkspaceLayoutMixin() {
|
|
|
400
366
|
self.homeUnassignedViews(ids);
|
|
401
367
|
}
|
|
402
368
|
else if (move.type === 'newTab') {
|
|
403
|
-
self.moveViewToNewTab(move.viewId
|
|
369
|
+
self.moveViewToNewTab(move.viewId);
|
|
404
370
|
}
|
|
405
371
|
else {
|
|
406
|
-
self.moveViewToSplit(move.viewId, 'row'
|
|
407
|
-
}
|
|
408
|
-
const home = tabContainingView(self.tree, move.viewId);
|
|
409
|
-
if (home) {
|
|
410
|
-
self.setActiveTab(home.panel.id, home.tab.id);
|
|
372
|
+
self.moveViewToSplit(move.viewId, 'row');
|
|
411
373
|
}
|
|
412
374
|
},
|
|
413
375
|
/**
|
|
414
376
|
* Re-arrange the whole workspace, one view per cell, in one of four
|
|
415
|
-
* shapes
|
|
416
|
-
* nothing for `orderViews` to apply.
|
|
377
|
+
* shapes, in `session.views` order.
|
|
417
378
|
*/
|
|
418
|
-
tileViews(mode
|
|
419
|
-
self.applyLayoutSpec(tileLayoutSpec(
|
|
379
|
+
tileViews(mode) {
|
|
380
|
+
self.applyLayoutSpec(tileLayoutSpec(sessionViewIds(self) ?? [], mode));
|
|
420
381
|
},
|
|
421
382
|
/**
|
|
422
383
|
* #action
|
|
@@ -440,6 +401,16 @@ export function WorkspaceLayoutMixin() {
|
|
|
440
401
|
host.orderViews(ids);
|
|
441
402
|
return ids;
|
|
442
403
|
},
|
|
404
|
+
}))
|
|
405
|
+
.actions(self => ({
|
|
406
|
+
afterAttach() {
|
|
407
|
+
addDisposer(self, autorun(function homeUnassignedViewsAutorun() {
|
|
408
|
+
const host = self;
|
|
409
|
+
if (host.views) {
|
|
410
|
+
self.homeUnassignedViews(host.views.map(v => v.id));
|
|
411
|
+
}
|
|
412
|
+
}, { name: 'HomeUnassignedViews' }));
|
|
413
|
+
},
|
|
443
414
|
}))
|
|
444
415
|
// A session saved with workspaces off showed its views stacked whatever
|
|
445
416
|
// layout it carried, so it opens as one cell holding them all
|
|
@@ -39,6 +39,7 @@ export interface TabHome {
|
|
|
39
39
|
export declare function findTab(node: LayoutTree, tabId: string): TabHome | undefined;
|
|
40
40
|
/** The tab a panel is showing: the one it names, or its first. */
|
|
41
41
|
export declare function activeTabIn(panel: PanelNode): TabNode | undefined;
|
|
42
|
+
export declare function activeTabOf(root: LayoutTree, panelId: string): TabNode | undefined;
|
|
42
43
|
export declare function tabContainingView(node: LayoutTree, viewId: string): TabHome | undefined;
|
|
43
44
|
export declare function panelContainingView(node: LayoutTree, viewId: string): PanelNode | undefined;
|
|
44
45
|
/**
|
|
@@ -93,9 +94,5 @@ export declare function homeViews(root: LayoutTree, viewIds: string[], activePan
|
|
|
93
94
|
* normalisation rule: only the gesture that emptied the panel calls it.
|
|
94
95
|
*/
|
|
95
96
|
export declare function pruneEmptyPanel(root: LayoutTree, panelId: string): LayoutTree;
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
* Closing a tab's last view leaves the tab standing, as a place to open the
|
|
99
|
-
* next one; only a move out of it prunes.
|
|
100
|
-
*/
|
|
101
|
-
export declare function pruneEmptyTabIn(root: LayoutTree, panelId: string, tabId: string): LayoutTree;
|
|
97
|
+
/** Remove a tab that holds no views; a tab with views stays. */
|
|
98
|
+
export declare function pruneEmptyTab(root: LayoutTree, tabId: string): LayoutTree;
|
|
@@ -31,6 +31,10 @@ export function findTab(node, tabId) {
|
|
|
31
31
|
export function activeTabIn(panel) {
|
|
32
32
|
return panel.tabs.find(t => t.id === panel.activeTabId) ?? panel.tabs[0];
|
|
33
33
|
}
|
|
34
|
+
export function activeTabOf(root, panelId) {
|
|
35
|
+
const panel = panels(root).find(p => p.id === panelId);
|
|
36
|
+
return panel && activeTabIn(panel);
|
|
37
|
+
}
|
|
34
38
|
export function tabContainingView(node, viewId) {
|
|
35
39
|
return homeOf(node, t => t.viewIds.includes(viewId));
|
|
36
40
|
}
|
|
@@ -271,16 +275,8 @@ export function pruneEmptyPanel(root, panelId) {
|
|
|
271
275
|
? removePanel(root, panelId)
|
|
272
276
|
: root;
|
|
273
277
|
}
|
|
274
|
-
/**
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*/
|
|
279
|
-
export function pruneEmptyTabIn(root, panelId, tabId) {
|
|
280
|
-
const found = findPanel(root, panelId);
|
|
281
|
-
if (!found || found.tabs.length <= 1) {
|
|
282
|
-
return root;
|
|
283
|
-
}
|
|
284
|
-
const tab = found.tabs.find(t => t.id === tabId);
|
|
285
|
-
return tab?.viewIds.length === 0 ? removeTab(root, tabId) : root;
|
|
278
|
+
/** Remove a tab that holds no views; a tab with views stays. */
|
|
279
|
+
export function pruneEmptyTab(root, tabId) {
|
|
280
|
+
const found = findTab(root, tabId);
|
|
281
|
+
return found?.tab.viewIds.length === 0 ? removeTab(root, tabId) : root;
|
|
286
282
|
}
|
|
@@ -10,12 +10,18 @@ function samePlace(a, b) {
|
|
|
10
10
|
a?.strip?.index === b?.strip?.index &&
|
|
11
11
|
a?.strip?.left === b?.strip?.left);
|
|
12
12
|
}
|
|
13
|
-
// a
|
|
13
|
+
// a drop `dropTabInPanel` / `dropTabInNewSplit` would leave as it was, which
|
|
14
14
|
// paints nothing either
|
|
15
15
|
function declines(layout, drag) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const home = layout.findTab(drag.tabId);
|
|
17
|
+
if (home?.panel.id !== drag.panelId) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (drag.strip) {
|
|
21
|
+
const from = home.panel.tabs.findIndex(t => t.id === drag.tabId);
|
|
22
|
+
return drag.strip.index === from || drag.strip.index === from + 1;
|
|
23
|
+
}
|
|
24
|
+
return drag.zone === 'center' || home.panel.tabs.length === 1;
|
|
19
25
|
}
|
|
20
26
|
/**
|
|
21
27
|
* Dragging a tab. Pointer events rather than HTML5 dnd, for capture; the
|
package/esm/index.d.ts
CHANGED
|
@@ -8,11 +8,13 @@ export type { AppRootModel } from './AppSession/index.ts';
|
|
|
8
8
|
export { RootAppMenuMixin } from './RootMenu/index.ts';
|
|
9
9
|
export { WorkspaceLayoutMixin, isSessionWithWorkspaceLayout, } from './WorkspaceLayout/index.ts';
|
|
10
10
|
export type { WorkspaceLayoutMixinType, WorkspaceLayout, LayoutTree, PanelNode, TabNode, LayoutSpecNode, PendingMove, TileMode, TabHome, } from './WorkspaceLayout/index.ts';
|
|
11
|
-
export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.ts';
|
|
12
|
-
export type { LayoutNode, LgvUrlInit, ParsedSessionSpec, ViewSpec, } from './SessionSpec/index.ts';
|
|
11
|
+
export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSnapshotUrl, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.ts';
|
|
12
|
+
export type { LayoutNode, LgvUrlInit, ParsedSessionSnapshotUrl, ParsedSessionSpec, ViewSpec, } from './SessionSpec/index.ts';
|
|
13
13
|
export type { SessionModelFactory } from './sessionModelFactory.ts';
|
|
14
|
-
export { deleteQueryParams, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.ts';
|
|
14
|
+
export { deleteQueryParams, queryParamsHref, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.ts';
|
|
15
15
|
export { processMutableMenuActions, resolveMenus } from './menus.ts';
|
|
16
|
+
export { formatSessionSummary, summarizeSession } from './sessionSummary.ts';
|
|
17
|
+
export type { SessionSummary } from './sessionSummary.ts';
|
|
16
18
|
export type { Menu, MenuAction } from './menus.ts';
|
|
17
19
|
export { createJbApi, ensureReExports, safeJson, sessionOf, undeliveredNotifications, waitReady, } from './JbApi/index.ts';
|
|
18
20
|
export type { JbApi } from './JbApi/index.ts';
|
package/esm/index.js
CHANGED
|
@@ -6,11 +6,12 @@ export { AssembliesMixin } from './Assemblies/index.js';
|
|
|
6
6
|
export { AppSessionMixin } from './AppSession/index.js';
|
|
7
7
|
export { RootAppMenuMixin } from './RootMenu/index.js';
|
|
8
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';
|
|
9
|
+
export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSnapshotUrl, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.js';
|
|
10
10
|
// URL params, for the two products that own their page (web, desktop). An
|
|
11
11
|
// embedded product must not rewrite its host's URL — see queryParams.ts
|
|
12
|
-
export { deleteQueryParams, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.js';
|
|
12
|
+
export { deleteQueryParams, queryParamsHref, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.js';
|
|
13
13
|
export { processMutableMenuActions, resolveMenus } from './menus.js';
|
|
14
|
+
export { formatSessionSummary, summarizeSession } from './sessionSummary.js';
|
|
14
15
|
// The helper library an agent drives the app through: desktop serves it to
|
|
15
16
|
// run_javascript over MCP, web hands it to the page as window.jb
|
|
16
17
|
export { createJbApi, ensureReExports, safeJson, sessionOf, undeliveredNotifications, waitReady, } from './JbApi/index.js';
|
package/esm/queryParams.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export declare function readQueryParams<T extends string>(keys: T[]): Record<T,
|
|
|
2
2
|
export declare function readAllQueryParams(url?: URL | Location): URLSearchParams;
|
|
3
3
|
export declare function deleteQueryParams(keys: readonly string[]): void;
|
|
4
4
|
export declare function setQueryParams(values: Record<string, string | undefined>): void;
|
|
5
|
+
/** The page's own URL with `values` applied, for a link to another state. */
|
|
6
|
+
export declare function queryParamsHref(values: Record<string, string | undefined>): string;
|
|
5
7
|
/**
|
|
6
8
|
* Read one param and keep re-rendering as it changes, including across the
|
|
7
9
|
* `replaceState` writes above, which fire no event of their own.
|
package/esm/queryParams.js
CHANGED
|
@@ -23,7 +23,7 @@ function subscribe(callback) {
|
|
|
23
23
|
}
|
|
24
24
|
// Writes params back into `loc`, leaving the other URL component untouched — the
|
|
25
25
|
// URL hash/search setters each replace only their own component.
|
|
26
|
-
function
|
|
26
|
+
function urlWith(loc, params) {
|
|
27
27
|
const url = new URL(window.location.href);
|
|
28
28
|
const str = params.toString();
|
|
29
29
|
if (loc === 'hash') {
|
|
@@ -32,7 +32,10 @@ function writeParams(loc, params) {
|
|
|
32
32
|
else {
|
|
33
33
|
url.search = str;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
return url.href;
|
|
36
|
+
}
|
|
37
|
+
function writeParams(loc, params) {
|
|
38
|
+
window.history.replaceState(null, '', urlWith(loc, params));
|
|
36
39
|
// Copied, which this note has claimed while the loop read the live set. A
|
|
37
40
|
// listener is React's store-change callback, and React may flush the re-render
|
|
38
41
|
// synchronously inside it, so the set can be mutated mid-notify: an unmounted
|
|
@@ -67,8 +70,7 @@ export function deleteQueryParams(keys) {
|
|
|
67
70
|
}
|
|
68
71
|
writeParams(loc, params);
|
|
69
72
|
}
|
|
70
|
-
|
|
71
|
-
const loc = paramLocation();
|
|
73
|
+
function paramsWith(loc, values) {
|
|
72
74
|
const params = readParams(loc);
|
|
73
75
|
for (const [key, value] of Object.entries(values)) {
|
|
74
76
|
if (value === undefined) {
|
|
@@ -78,7 +80,16 @@ export function setQueryParams(values) {
|
|
|
78
80
|
params.set(key, value);
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
|
-
|
|
83
|
+
return params;
|
|
84
|
+
}
|
|
85
|
+
export function setQueryParams(values) {
|
|
86
|
+
const loc = paramLocation();
|
|
87
|
+
writeParams(loc, paramsWith(loc, values));
|
|
88
|
+
}
|
|
89
|
+
/** The page's own URL with `values` applied, for a link to another state. */
|
|
90
|
+
export function queryParamsHref(values) {
|
|
91
|
+
const loc = paramLocation();
|
|
92
|
+
return urlWith(loc, paramsWith(loc, values));
|
|
82
93
|
}
|
|
83
94
|
/**
|
|
84
95
|
* Read one param and keep re-rendering as it changes, including across the
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a saved session holds, for telling saved sessions apart in a recent
|
|
3
|
+
* list without opening them. Read off the snapshot rather than a live model,
|
|
4
|
+
* so it can be taken wherever the snapshot is written.
|
|
5
|
+
*/
|
|
6
|
+
export interface SessionSummary {
|
|
7
|
+
assemblies: string[];
|
|
8
|
+
views: number;
|
|
9
|
+
tracks: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Walks every view, including the subviews a comparative view nests, so a
|
|
13
|
+
* synteny view's assemblies and tracks count the same as two plain views'.
|
|
14
|
+
*/
|
|
15
|
+
export declare function summarizeSession(snap: unknown): SessionSummary;
|
|
16
|
+
export declare function formatSessionSummary({ assemblies, views, tracks, }: SessionSummary): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function isRecord(x) {
|
|
2
|
+
return typeof x === 'object' && x !== null;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Walks every view, including the subviews a comparative view nests, so a
|
|
6
|
+
* synteny view's assemblies and tracks count the same as two plain views'.
|
|
7
|
+
*/
|
|
8
|
+
export function summarizeSession(snap) {
|
|
9
|
+
const assemblies = new Set();
|
|
10
|
+
let tracks = 0;
|
|
11
|
+
const visit = (node) => {
|
|
12
|
+
if (Array.isArray(node)) {
|
|
13
|
+
for (const child of node) {
|
|
14
|
+
visit(child);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else if (isRecord(node)) {
|
|
18
|
+
for (const [key, value] of Object.entries(node)) {
|
|
19
|
+
if (key === 'assemblyName' && typeof value === 'string') {
|
|
20
|
+
assemblies.add(value);
|
|
21
|
+
}
|
|
22
|
+
else if (key === 'assemblyNames' && Array.isArray(value)) {
|
|
23
|
+
for (const name of value) {
|
|
24
|
+
if (typeof name === 'string') {
|
|
25
|
+
assemblies.add(name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (key === 'tracks' && Array.isArray(value)) {
|
|
30
|
+
tracks += value.length;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
visit(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const views = isRecord(snap) && Array.isArray(snap.views) ? snap.views : [];
|
|
39
|
+
visit(views);
|
|
40
|
+
return { assemblies: [...assemblies], views: views.length, tracks };
|
|
41
|
+
}
|
|
42
|
+
function plural(n, word) {
|
|
43
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
44
|
+
}
|
|
45
|
+
export function formatSessionSummary({ assemblies, views, tracks, }) {
|
|
46
|
+
const shown = assemblies.slice(0, 2).join(', ');
|
|
47
|
+
const more = assemblies.length - 2;
|
|
48
|
+
return views
|
|
49
|
+
? [
|
|
50
|
+
more > 0 ? `${shown} +${more} more` : shown,
|
|
51
|
+
plural(views, 'view'),
|
|
52
|
+
tracks ? plural(tracks, 'track') : '',
|
|
53
|
+
]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join(' · ')
|
|
56
|
+
: 'No views';
|
|
57
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
declare const DrawerHeaderHelpDialog: ({ onClose, helpText, }: {
|
|
2
2
|
onClose: () => void;
|
|
3
3
|
helpText: React.ReactNode;
|
|
4
|
-
})
|
|
4
|
+
}) => import("react").JSX.Element;
|
|
5
|
+
export default DrawerHeaderHelpDialog;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import InfoDialog from '@jbrowse/core/ui/InfoDialog';
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
|
+
import { observer } from 'mobx-react';
|
|
4
5
|
const useStyles = makeStyles()({
|
|
5
6
|
max: {
|
|
6
7
|
minWidth: '40em',
|
|
7
8
|
},
|
|
8
9
|
});
|
|
9
|
-
|
|
10
|
+
const DrawerHeaderHelpDialog = observer(function DrawerHeaderHelpDialog({ onClose, helpText, }) {
|
|
10
11
|
const { classes } = useStyles();
|
|
11
12
|
return (_jsx(InfoDialog, { open: true, onClose: onClose, title: "Help", maxWidth: "xl", onClick: e => {
|
|
12
13
|
e.stopPropagation();
|
|
13
14
|
}, children: _jsx("div", { className: classes.max, children: helpText }) }));
|
|
14
|
-
}
|
|
15
|
+
});
|
|
16
|
+
export default DrawerHeaderHelpDialog;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
declare const JBrowseTabMenu: ({ onRename, onClose, }: {
|
|
2
2
|
onRename: () => void;
|
|
3
3
|
onClose: () => void;
|
|
4
|
-
})
|
|
4
|
+
}) => import("react").JSX.Element;
|
|
5
|
+
export default JBrowseTabMenu;
|
|
@@ -5,6 +5,7 @@ import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
|
5
5
|
import CloseIcon from '@mui/icons-material/Close';
|
|
6
6
|
import EditIcon from '@mui/icons-material/Edit';
|
|
7
7
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
8
|
+
import { observer } from 'mobx-react';
|
|
8
9
|
const useStyles = makeStyles()({
|
|
9
10
|
// hidden until the tab is hovered or focused, by a rule in `TabStrip` keyed
|
|
10
11
|
// on the `jbrowse-tab-menu` class — hence the plain class name beside the
|
|
@@ -30,7 +31,7 @@ const useStyles = makeStyles()({
|
|
|
30
31
|
fontSize: 14,
|
|
31
32
|
},
|
|
32
33
|
});
|
|
33
|
-
|
|
34
|
+
const JBrowseTabMenu = observer(function JBrowseTabMenu({ onRename, onClose, }) {
|
|
34
35
|
const { classes, cx } = useStyles();
|
|
35
36
|
const [open, setOpen] = useState(false);
|
|
36
37
|
return (_jsx("div", { className: cx('jbrowse-tab-menu', classes.tabIcons, open && classes.open),
|
|
@@ -55,4 +56,5 @@ export default function JBrowseTabMenu({ onRename, onClose, }) {
|
|
|
55
56
|
onClick: onClose,
|
|
56
57
|
},
|
|
57
58
|
], tooltip: "Tab menu", size: "small", className: classes.tabIcon, setOpen: setOpen, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
|
|
58
|
-
}
|
|
59
|
+
});
|
|
60
|
+
export default JBrowseTabMenu;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { ThemeSwitchSession } from './types.ts';
|
|
2
2
|
/**
|
|
3
|
-
* A light/dark switch for a session that follows the OS
|
|
4
|
-
* drawn. A click holds the other mode, a second click
|
|
5
|
-
* session on an explicit light
|
|
6
|
-
*
|
|
7
|
-
* route into dark.
|
|
3
|
+
* A light/dark switch for a session that follows the OS or sits on dark,
|
|
4
|
+
* showing the mode drawn. A click holds the other mode, a second click returns
|
|
5
|
+
* to the setting. A session on an explicit light gets nothing, so a reader who
|
|
6
|
+
* never asked for dark is offered no route into it.
|
|
8
7
|
*/
|
|
9
8
|
declare const ThemeModeButton: ({ session, }: {
|
|
10
9
|
session: ThemeSwitchSession;
|
|
@@ -4,30 +4,37 @@ import LightModeIcon from '@mui/icons-material/LightMode';
|
|
|
4
4
|
import { IconButton, Tooltip } from '@mui/material';
|
|
5
5
|
import { observer } from 'mobx-react';
|
|
6
6
|
/**
|
|
7
|
-
* A light/dark switch for a session that follows the OS
|
|
8
|
-
* drawn. A click holds the other mode, a second click
|
|
9
|
-
* session on an explicit light
|
|
10
|
-
*
|
|
11
|
-
* route into dark.
|
|
7
|
+
* A light/dark switch for a session that follows the OS or sits on dark,
|
|
8
|
+
* showing the mode drawn. A click holds the other mode, a second click returns
|
|
9
|
+
* to the setting. A session on an explicit light gets nothing, so a reader who
|
|
10
|
+
* never asked for dark is offered no route into it.
|
|
12
11
|
*/
|
|
13
12
|
const ThemeModeButton = observer(function ThemeModeButton({ session, }) {
|
|
14
13
|
const dark = session.themeIsDark;
|
|
15
14
|
const drawnMode = dark ? 'dark' : 'light';
|
|
16
15
|
// A palette pinned to its own mode, or Dark Reader, draws dark whatever the
|
|
17
16
|
// OS says, so a click here would change nothing.
|
|
18
|
-
const
|
|
17
|
+
const switchable = session.themeMode !== 'light' &&
|
|
19
18
|
!session.darkReaderDark &&
|
|
20
19
|
drawnMode === session.effectiveThemeMode;
|
|
21
|
-
if (!
|
|
20
|
+
if (!switchable) {
|
|
22
21
|
return null;
|
|
23
22
|
}
|
|
24
23
|
const held = session.systemThemeOverride;
|
|
25
24
|
const otherMode = dark ? 'light' : 'dark';
|
|
26
25
|
const Icon = dark ? DarkModeIcon : LightModeIcon;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const system = session.themeMode === 'system';
|
|
27
|
+
const modeName = dark ? 'Dark' : 'Light';
|
|
28
|
+
const label = system
|
|
29
|
+
? held
|
|
30
|
+
? `${modeName} until your system theme changes`
|
|
31
|
+
: `Following your system theme (${drawnMode})`
|
|
32
|
+
: held
|
|
33
|
+
? `${modeName} instead of your dark setting`
|
|
34
|
+
: modeName;
|
|
35
|
+
const action = held
|
|
36
|
+
? `to ${system ? 'follow it' : 'return'} (${otherMode})`
|
|
37
|
+
: `for ${otherMode}`;
|
|
31
38
|
return (_jsx(Tooltip, { title: `${label}. Click ${action}.`, arrow: true, children: _jsx(IconButton, { "data-testid": "theme-mode-button", "aria-label": label, color: "inherit", size: "small", onClick: () => {
|
|
32
39
|
session.setSystemThemeOverride(held ? undefined : otherMode);
|
|
33
40
|
}, children: _jsx(Icon, { fontSize: "small" }) }) }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
+
import LoadingEllipses from '@jbrowse/core/ui/LoadingEllipses';
|
|
3
4
|
import { useFocusOnInteraction, useWidthSetter } from '@jbrowse/core/util/hooks';
|
|
4
5
|
import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
6
|
import { isAlive } from '@jbrowse/mobx-state-tree';
|
|
@@ -47,6 +48,11 @@ function viewPhase(view) {
|
|
|
47
48
|
: 'ready';
|
|
48
49
|
}
|
|
49
50
|
const useStyles = makeStyles()(theme => ({
|
|
51
|
+
// ViewLoadingScreen's label and place, which the body shows once mounted
|
|
52
|
+
pending: {
|
|
53
|
+
paddingTop: 20,
|
|
54
|
+
textAlign: 'center',
|
|
55
|
+
},
|
|
50
56
|
viewContainer: {
|
|
51
57
|
margin: theme.spacing(0.5),
|
|
52
58
|
padding: `0 ${theme.spacing(1)} ${theme.spacing(1)}`,
|
|
@@ -148,6 +154,6 @@ const ViewContainer = observer(function ViewContainer({ view, session, scrollOnM
|
|
|
148
154
|
// landmark list, so "jump to the second view" stops being a Tab count.
|
|
149
155
|
role: "region", "aria-label": viewTitle(view, r => assemblyManager.getDisplayName(r)), "data-testid": `view-container-${view.id}`, "data-view-phase": viewPhase(view), children: [_jsx(ViewHeader, { view: view, className: backgroundColorClassName, scrollOnMount: scrollOnMount }), _jsx(Paper, { elevation: 0, children: _jsx("div", { ref: bodyRef, style: reserveSpace
|
|
150
156
|
? { height: measuredHeight ?? viewHeight(view) }
|
|
151
|
-
: undefined, children: showBody ? _jsx(ViewWrapper, { view: view, session: session }) : null }) })] }));
|
|
157
|
+
: undefined, children: showBody ? (_jsx(ViewWrapper, { view: view, session: session })) : reserveSpace && measuredHeight === undefined ? (_jsx(LoadingEllipses, { variant: "h6", busy: false, className: classes.pending })) : null }) })] }));
|
|
152
158
|
});
|
|
153
159
|
export default ViewContainer;
|