@jbrowse/app-core 5.0.0-beta.1 → 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.
- package/esm/AppSession/AppSessionMixin.d.ts +42 -316
- package/esm/Assemblies/AssembliesMixin.d.ts +172 -103
- package/esm/Assemblies/AssembliesMixin.js +13 -8
- package/esm/HistoryManagement/index.d.ts +3 -11
- package/esm/HistoryManagement/index.js +24 -28
- package/esm/JBrowseConfig/RootConfiguration.d.ts +80 -124
- package/esm/JBrowseConfig/RootConfiguration.js +5 -11
- package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
- package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
- package/esm/JBrowseConfig/index.d.ts +89 -83
- package/esm/JBrowseConfig/index.js +6 -4
- package/esm/JBrowseModel/index.d.ts +93 -96
- package/esm/JBrowseModel/index.js +43 -43
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +2767 -0
- package/esm/JbApi/jbApi.js +1383 -0
- package/esm/RootMenu/index.d.ts +2 -11
- package/esm/RootMenu/index.js +0 -11
- package/esm/SessionSpec/index.d.ts +1 -1
- package/esm/SessionSpec/index.js +1 -1
- package/esm/SessionSpec/lgvUrlInit.js +5 -2
- package/esm/SessionSpec/loadSessionSpec.d.ts +56 -2
- package/esm/SessionSpec/loadSessionSpec.js +285 -123
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +2 -8
- package/esm/SessionSpec/parseSessionSpecUrl.js +2 -17
- package/esm/SessionSpec/types.d.ts +16 -12
- package/esm/WorkspaceLayout/WorkspaceContainer.js +4 -2
- package/esm/WorkspaceLayout/WorkspaceTab.js +2 -6
- package/esm/WorkspaceLayout/model.d.ts +24 -4
- package/esm/WorkspaceLayout/model.js +76 -17
- package/esm/WorkspaceLayout/spec.d.ts +40 -10
- package/esm/WorkspaceLayout/spec.js +81 -8
- package/esm/index.d.ts +4 -2
- package/esm/index.js +4 -1
- package/esm/menus.d.ts +15 -29
- package/esm/menus.js +76 -92
- package/esm/queryParams.d.ts +1 -1
- package/esm/queryParams.js +10 -9
- package/esm/ui/App/App.js +18 -11
- package/esm/ui/App/AppToolbar.js +23 -8
- package/esm/ui/App/ClassicViewsContainer.d.ts +2 -2
- package/esm/ui/App/DialogQueue.d.ts +2 -2
- package/esm/ui/App/DialogQueue.js +3 -3
- package/esm/ui/App/DrawerHeader.js +2 -3
- package/esm/ui/App/ThemeModeButton.d.ts +12 -0
- package/esm/ui/App/ThemeModeButton.js +34 -0
- package/esm/ui/App/ViewButtons.js +2 -2
- package/esm/ui/App/ViewContainer.js +1 -17
- package/esm/ui/App/ViewContainerTitle.js +15 -12
- package/esm/ui/App/ViewLauncher.js +1 -1
- package/esm/ui/App/ViewMenu.js +2 -2
- package/esm/ui/App/ViewStack.js +7 -1
- package/esm/ui/App/ViewsContainer.d.ts +2 -4
- package/esm/ui/App/ViewsContainer.js +3 -9
- package/esm/ui/App/index.d.ts +1 -1
- package/esm/ui/App/index.js +1 -1
- package/esm/ui/App/lazyParts.d.ts +32 -0
- package/esm/ui/App/lazyParts.js +8 -0
- package/esm/ui/App/preloadAppShell.d.ts +14 -0
- package/esm/ui/App/preloadAppShell.js +41 -0
- package/esm/ui/App/types.d.ts +12 -7
- package/esm/ui/App/useViewVisibility.d.ts +9 -8
- package/esm/ui/App/useViewVisibility.js +11 -10
- package/esm/ui/App/viewTitle.d.ts +10 -3
- package/esm/ui/App/viewTitle.js +13 -5
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +7 -7
- package/esm/ui/App/AppReadyMarker.d.ts +0 -34
- package/esm/ui/App/AppReadyMarker.js +0 -56
- package/esm/ui/App/DrawerControls.d.ts +0 -1
- package/esm/ui/App/DrawerControls.js +0 -1
- package/esm/ui/App/copyView.d.ts +0 -1
- package/esm/ui/App/copyView.js +0 -29
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionSpec } from './loadSessionSpec.ts';
|
|
2
2
|
export interface ParsedSessionSpec {
|
|
3
3
|
configUrl?: string;
|
|
4
|
-
spec:
|
|
5
|
-
views: ViewSpec[];
|
|
6
|
-
sessionAssemblies?: Record<string, unknown>[];
|
|
7
|
-
sessionConnections?: Record<string, unknown>[];
|
|
8
|
-
sessionTracks?: Record<string, unknown>[];
|
|
9
|
-
layout?: LayoutNode;
|
|
10
|
-
};
|
|
4
|
+
spec: Omit<SessionSpec, 'sessionName'>;
|
|
11
5
|
sessionName?: string;
|
|
12
6
|
}
|
|
13
7
|
export declare function parseSessionSpecUrl(input: string): ParsedSessionSpec;
|
|
@@ -1,20 +1,5 @@
|
|
|
1
|
+
import { readAllQueryParams } from '../queryParams.js';
|
|
1
2
|
import { buildLgvInitFromParams, hubConnectionSpec, readHubUrlParam, } from './lgvUrlInit.js';
|
|
2
|
-
// jbrowse-web keeps its URL params in the hash fragment XOR the query string
|
|
3
|
-
// (the inline `encoded-`/`json-`/`spec-` sessions live in the hash to dodge the
|
|
4
|
-
// server request-line limit; short `share-` links stay in the query). Read from
|
|
5
|
-
// whichever the link uses so a pasted hash-form link parses the same as a
|
|
6
|
-
// query-form one, instead of tripping the "no session in it" error below.
|
|
7
|
-
//
|
|
8
|
-
// This MUST stay identical to jbrowse-web's own reader (`useQueryParam.ts`
|
|
9
|
-
// `paramLocation`: `hash.includes('=') ? hash : search`) — that module's whole
|
|
10
|
-
// point, and this file's, is that a URL resolves the same everywhere. The XOR is
|
|
11
|
-
// a documented invariant its producers (`buildShareUrl`) guarantee, so a
|
|
12
|
-
// both-present link is a non-case; don't "harden" this into a merge that would
|
|
13
|
-
// diverge from the canonical parser.
|
|
14
|
-
function linkParams(url) {
|
|
15
|
-
const hash = url.hash.slice(1);
|
|
16
|
-
return new URLSearchParams(hash.includes('=') ? hash : url.search);
|
|
17
|
-
}
|
|
18
3
|
// A `jbrowse://open?url=<web link>` wrapper carries a JBrowse Web url for
|
|
19
4
|
// Desktop's protocol handler to unwrap. Every page that offers an "Open in
|
|
20
5
|
// Desktop" button puts that link right next to the plain web one — the docs'
|
|
@@ -157,7 +142,7 @@ export function parseSessionSpecUrl(input) {
|
|
|
157
142
|
catch {
|
|
158
143
|
throw new Error(`Not a URL: ${input}`);
|
|
159
144
|
}
|
|
160
|
-
const params =
|
|
145
|
+
const params = readAllQueryParams(url);
|
|
161
146
|
const session = params.get('session');
|
|
162
147
|
if (!session) {
|
|
163
148
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LayoutSpecNode } from '../WorkspaceLayout/spec.ts';
|
|
1
2
|
import type { TrackInit } from '@jbrowse/core/util/tracks';
|
|
2
3
|
export interface ViewSpec {
|
|
3
4
|
type: string;
|
|
@@ -5,16 +6,24 @@ export interface ViewSpec {
|
|
|
5
6
|
displayName?: string;
|
|
6
7
|
tracks?: TrackInit[];
|
|
7
8
|
assembly?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The live view's spelling of the key above, accepted because that is where
|
|
11
|
+
* a spec gets written from: one name is the `assembly`, and several are
|
|
12
|
+
* refused naming it.
|
|
13
|
+
*/
|
|
14
|
+
assemblyNames?: string[];
|
|
8
15
|
loc?: string;
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated v4's nesting. Write every setting directly on the view object;
|
|
18
|
+
* this is unwrapped on the way in and warns.
|
|
19
|
+
*/
|
|
20
|
+
init?: Record<string, unknown>;
|
|
9
21
|
}
|
|
10
22
|
/**
|
|
11
|
-
*
|
|
23
|
+
* A spec's `layout`: the one layout shape, with a leaf's `views` counting into
|
|
24
|
+
* the spec's own `views` array (an index names every view that entry created)
|
|
25
|
+
* or naming a view id the spec pinned with `id`.
|
|
12
26
|
*
|
|
13
|
-
* A LayoutNode is either:
|
|
14
|
-
* - A panel (has `views` array) - displays views stacked vertically
|
|
15
|
-
* - A container (has `children` array) - arranges children horizontally or vertically
|
|
16
|
-
*
|
|
17
|
-
* Example - horizontal split with custom sizes:
|
|
18
27
|
* ```json
|
|
19
28
|
* {
|
|
20
29
|
* "direction": "horizontal",
|
|
@@ -25,9 +34,4 @@ export interface ViewSpec {
|
|
|
25
34
|
* }
|
|
26
35
|
* ```
|
|
27
36
|
*/
|
|
28
|
-
export
|
|
29
|
-
views?: number[];
|
|
30
|
-
direction?: 'horizontal' | 'vertical' | 'tabs';
|
|
31
|
-
children?: LayoutNode[];
|
|
32
|
-
size?: number;
|
|
33
|
-
}
|
|
37
|
+
export type LayoutNode = LayoutSpecNode;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense,
|
|
2
|
+
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
4
|
import { autorun } from 'mobx';
|
|
5
5
|
import { observer } from 'mobx-react';
|
|
6
6
|
import ViewStack from '../ui/App/ViewStack.js';
|
|
7
|
+
import { ViewLauncher } from '../ui/App/lazyParts.js';
|
|
7
8
|
import { LayoutRenderer } from './LayoutRenderer.js';
|
|
8
9
|
import { WorkspacePanelActions } from './WorkspacePanelActions.js';
|
|
9
10
|
import { WorkspaceTab } from './WorkspaceTab.js';
|
|
10
11
|
import { useLayoutDrag } from './useLayoutDrag.js';
|
|
11
|
-
const ViewLauncher = lazy(() => import('../ui/App/ViewLauncher.js'));
|
|
12
12
|
const useStyles = makeStyles()(theme => ({
|
|
13
13
|
container: {
|
|
14
14
|
height: '100%',
|
|
@@ -41,11 +41,13 @@ const useStyles = makeStyles()(theme => ({
|
|
|
41
41
|
export const WorkspaceContainer = observer(function WorkspaceContainer({ session, }) {
|
|
42
42
|
const { classes } = useStyles();
|
|
43
43
|
const { drag, handlers } = useLayoutDrag(session);
|
|
44
|
+
// #region autorunInEffect
|
|
44
45
|
useEffect(() => autorun(() => {
|
|
45
46
|
// reads session.views itself, so it re-runs when the view set changes;
|
|
46
47
|
// homeUnassignedViews is an action and would not be tracked from inside
|
|
47
48
|
session.homeUnassignedViews(session.views.map(v => v.id));
|
|
48
49
|
}), [session]);
|
|
50
|
+
// #endregion
|
|
49
51
|
// The layout does not own views, so closing anything that holds them is
|
|
50
52
|
// explicitly the pair. Stated ONCE, here, because every spelling of it is a
|
|
51
53
|
// chance to drop the node and leave its views in the session forever — and
|
|
@@ -4,6 +4,7 @@ import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
|
4
4
|
import { InputBase, Typography } from '@mui/material';
|
|
5
5
|
import { observer } from 'mobx-react';
|
|
6
6
|
import JBrowseTabMenu from '../ui/App/JBrowseTabMenu.js';
|
|
7
|
+
import { viewName } from '../ui/App/viewTitle.js';
|
|
7
8
|
const useStyles = makeStyles()(theme => ({
|
|
8
9
|
// Only the label and its menu. The background, the selected state and the
|
|
9
10
|
// hover live on the strip's own `[role=tab]` wrapper, which is where the
|
|
@@ -52,12 +53,7 @@ export function tabDisplayName(tab, views, session) {
|
|
|
52
53
|
return 'Empty';
|
|
53
54
|
}
|
|
54
55
|
if (views.length === 1) {
|
|
55
|
-
|
|
56
|
-
return (view.displayName ||
|
|
57
|
-
view.assemblyNames
|
|
58
|
-
?.map(r => session.assemblyManager.getDisplayName(r))
|
|
59
|
-
.join(',') ||
|
|
60
|
-
'View');
|
|
56
|
+
return viewName(views[0], r => session.assemblyManager.getDisplayName(r));
|
|
61
57
|
}
|
|
62
58
|
return `${views.length} views`;
|
|
63
59
|
}
|
|
@@ -182,6 +182,14 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
|
|
|
182
182
|
/**
|
|
183
183
|
* Arrange the workspace as a spec states.
|
|
184
184
|
*
|
|
185
|
+
* The spec is the same shape a session spec's `layout` takes: a leaf's
|
|
186
|
+
* `views` names views by index into `session.views` or by id, so
|
|
187
|
+
* `{ direction: 'horizontal', children: [{ views: [0] }, { views: [1] }] }`
|
|
188
|
+
* means here what it means in a link. A leaf spelled any other way, an
|
|
189
|
+
* index past the end or an id no view has throws rather than arranging
|
|
190
|
+
* nothing — an untyped caller used to collapse the workspace into one
|
|
191
|
+
* blank tab that way.
|
|
192
|
+
*
|
|
185
193
|
* There is no `init` property and no standing request: the spec is
|
|
186
194
|
* converted and *becomes* the layout, here and now. `init` existed only
|
|
187
195
|
* because dockview had to be told, could not be told before it mounted,
|
|
@@ -192,16 +200,16 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
|
|
|
192
200
|
/**
|
|
193
201
|
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
194
202
|
*
|
|
195
|
-
* `allViewIds` is EVERY view in the session,
|
|
196
|
-
*
|
|
203
|
+
* `allViewIds` is EVERY view in the session, defaulting to the host's
|
|
204
|
+
* own list — homing drops any view the list does not name, so the
|
|
197
205
|
* `[viewId]` default this used to carry unhomed all the others.
|
|
198
206
|
*/
|
|
199
|
-
moveViewToNewTab(viewId: string, allViewIds
|
|
207
|
+
moveViewToNewTab(viewId: string, allViewIds?: string[]): string | undefined;
|
|
200
208
|
/**
|
|
201
209
|
* ViewMenu's "move to split view": the view leaves for a new cell.
|
|
202
210
|
* `allViewIds` is every view in the session — see `moveViewToNewTab`.
|
|
203
211
|
*/
|
|
204
|
-
moveViewToSplitRight(viewId: string, allViewIds
|
|
212
|
+
moveViewToSplitRight(viewId: string, allViewIds?: string[]): string | undefined;
|
|
205
213
|
homeUnassignedViews(viewIds: string[]): void;
|
|
206
214
|
} & {
|
|
207
215
|
/**
|
|
@@ -233,6 +241,18 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
|
|
|
233
241
|
* session spec's layout there is nothing for `orderViews` to apply.
|
|
234
242
|
*/
|
|
235
243
|
tileViews(mode: TileMode, allViewIds: string[]): void;
|
|
244
|
+
/**
|
|
245
|
+
* #action
|
|
246
|
+
* Arrange the session's views into panels. Calls `applyLayoutSpec`,
|
|
247
|
+
* turns workspaces mode on for this session (a layout renders nowhere
|
|
248
|
+
* else), and orders `session.views` to the spec's top-to-bottom
|
|
249
|
+
* order, which tabs read their order from. Without the last two steps
|
|
250
|
+
* the layout applies but does not display, or the tabs appear in the
|
|
251
|
+
* wrong order. Leaves take view ids or indexes into `session.views`; a
|
|
252
|
+
* layout that seats no view throws rather than leaving a blank tab. A
|
|
253
|
+
* session spec's `layout` and an agent's live re-layout both call it.
|
|
254
|
+
*/
|
|
255
|
+
layoutViews(spec: LayoutSpecNode): string[];
|
|
236
256
|
}, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
|
|
237
257
|
export type WorkspaceLayoutMixinType = ReturnType<typeof WorkspaceLayoutMixin>;
|
|
238
258
|
export interface WorkspaceLayout extends Instance<WorkspaceLayoutMixinType> {
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { createElementId } from '@jbrowse/core/util/types/mst';
|
|
2
2
|
import { cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
|
|
3
|
-
import { specForPendingMove, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
|
|
3
|
+
import { resolveLayoutSpec, specForPendingMove, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
|
|
4
4
|
import { activeTabIn, addTab, addViewToTab, findTab, homeViews, moveTabToPanel, normalize, panelContainingView, panels, pruneEmptyPanel, pruneEmptyTabIn, removePanel, removeTab, removeView, renameTab, setActiveTab, setSizes, splitPanel, tabContainingView, tabs, } from './tree.js';
|
|
5
|
+
function sessionViewIds(self) {
|
|
6
|
+
return self.views?.map(v => v.id);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A caller-supplied view list narrowed to the views the session actually has.
|
|
10
|
+
*
|
|
11
|
+
* The list is the caller's snapshot, and `applyLayoutSpec` refuses an id no
|
|
12
|
+
* view has — so a view added after the list was built, or removed between
|
|
13
|
+
* building it and the call, would throw out of a sugar action no plugin wraps.
|
|
14
|
+
* The arrangement a stale name asks for is still the one it means for every
|
|
15
|
+
* view that IS there, so the name is dropped rather than the action. A repeat
|
|
16
|
+
* is dropped for the same reason: it would seat one view in two cells, which
|
|
17
|
+
* the resolver also refuses.
|
|
18
|
+
*/
|
|
19
|
+
function liveViewIds(self, allViewIds) {
|
|
20
|
+
const live = sessionViewIds(self);
|
|
21
|
+
return [
|
|
22
|
+
...new Set(live ? allViewIds.filter(id => live.includes(id)) : allViewIds),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* #stateModel WorkspaceLayoutMixin
|
|
7
27
|
*
|
|
@@ -210,6 +230,13 @@ export function WorkspaceLayoutMixin() {
|
|
|
210
230
|
* Returns where the view came from, or `undefined` if it is not in the
|
|
211
231
|
* session, in which case nothing is applied.
|
|
212
232
|
*/
|
|
233
|
+
function everyViewId(action, allViewIds) {
|
|
234
|
+
const ids = allViewIds ?? sessionViewIds(self);
|
|
235
|
+
if (!ids) {
|
|
236
|
+
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`);
|
|
237
|
+
}
|
|
238
|
+
return ids;
|
|
239
|
+
}
|
|
213
240
|
function rehomeView(viewId, allViewIds, place) {
|
|
214
241
|
const tree = home(self.tree, allViewIds);
|
|
215
242
|
const from = tabContainingView(tree, viewId);
|
|
@@ -400,6 +427,14 @@ export function WorkspaceLayoutMixin() {
|
|
|
400
427
|
/**
|
|
401
428
|
* Arrange the workspace as a spec states.
|
|
402
429
|
*
|
|
430
|
+
* The spec is the same shape a session spec's `layout` takes: a leaf's
|
|
431
|
+
* `views` names views by index into `session.views` or by id, so
|
|
432
|
+
* `{ direction: 'horizontal', children: [{ views: [0] }, { views: [1] }] }`
|
|
433
|
+
* means here what it means in a link. A leaf spelled any other way, an
|
|
434
|
+
* index past the end or an id no view has throws rather than arranging
|
|
435
|
+
* nothing — an untyped caller used to collapse the workspace into one
|
|
436
|
+
* blank tab that way.
|
|
437
|
+
*
|
|
403
438
|
* There is no `init` property and no standing request: the spec is
|
|
404
439
|
* converted and *becomes* the layout, here and now. `init` existed only
|
|
405
440
|
* because dockview had to be told, could not be told before it mounted,
|
|
@@ -407,20 +442,21 @@ export function WorkspaceLayoutMixin() {
|
|
|
407
442
|
* from the layout living somewhere this action could not reach.
|
|
408
443
|
*/
|
|
409
444
|
applyLayoutSpec(spec) {
|
|
410
|
-
|
|
445
|
+
const resolved = resolveLayoutSpec(spec, sessionViewIds(self));
|
|
446
|
+
apply(treeFromSpec(resolved, nextId));
|
|
411
447
|
self.activePanelId = panels(self.tree)[0]?.id;
|
|
412
|
-
return viewIdsInSpec(
|
|
448
|
+
return viewIdsInSpec(resolved);
|
|
413
449
|
},
|
|
414
450
|
/**
|
|
415
451
|
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
416
452
|
*
|
|
417
|
-
* `allViewIds` is EVERY view in the session,
|
|
418
|
-
*
|
|
453
|
+
* `allViewIds` is EVERY view in the session, defaulting to the host's
|
|
454
|
+
* own list — homing drops any view the list does not name, so the
|
|
419
455
|
* `[viewId]` default this used to carry unhomed all the others.
|
|
420
456
|
*/
|
|
421
457
|
moveViewToNewTab(viewId, allViewIds) {
|
|
422
458
|
const tab = { id: nextId('tab'), viewIds: [viewId] };
|
|
423
|
-
const from = rehomeView(viewId, allViewIds, (tree, at) => addTab(tree, at.panel.id, tab));
|
|
459
|
+
const from = rehomeView(viewId, everyViewId('moveViewToNewTab', allViewIds), (tree, at) => addTab(tree, at.panel.id, tab));
|
|
424
460
|
if (!from) {
|
|
425
461
|
return undefined;
|
|
426
462
|
}
|
|
@@ -439,7 +475,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
439
475
|
tabs: [{ id: tabId, viewIds: [viewId] }],
|
|
440
476
|
activeTabId: tabId,
|
|
441
477
|
};
|
|
442
|
-
const from = rehomeView(viewId, allViewIds, (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
|
|
478
|
+
const from = rehomeView(viewId, everyViewId('moveViewToSplitRight', allViewIds), (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
|
|
443
479
|
if (!from) {
|
|
444
480
|
return undefined;
|
|
445
481
|
}
|
|
@@ -480,21 +516,20 @@ export function WorkspaceLayoutMixin() {
|
|
|
480
516
|
* is spelled.
|
|
481
517
|
*/
|
|
482
518
|
setPendingMove(move, allViewIds) {
|
|
483
|
-
// The session's whole set of views, for the one entry point that cannot
|
|
484
|
-
// be handed it. Every other one takes it as an argument on purpose —
|
|
485
|
-
// this mixin owns the tree and knows nothing else — and this would too,
|
|
486
|
-
// if its caller were ours. Duck-typed because `views` is
|
|
487
|
-
// MultipleViewsSessionMixin's and composition is the only thing that
|
|
488
|
-
// puts the two together.
|
|
489
|
-
const { views } = self;
|
|
490
519
|
// Not `?? []`: homing drops every view the list does not name, so an
|
|
491
520
|
// empty one would answer "put this view beside nothing" and unhome the
|
|
492
521
|
// rest. Nothing to say is nothing to do.
|
|
493
|
-
const ids = allViewIds ??
|
|
522
|
+
const ids = allViewIds ?? sessionViewIds(self);
|
|
494
523
|
if (!move || !ids) {
|
|
495
524
|
return;
|
|
496
525
|
}
|
|
497
|
-
self
|
|
526
|
+
const known = liveViewIds(self, ids);
|
|
527
|
+
// Nothing to move: the view named is not in the session, so there is
|
|
528
|
+
// no arrangement to state about it.
|
|
529
|
+
if (sessionViewIds(self) && !known.includes(move.viewId)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
self.applyLayoutSpec(specForPendingMove(move, known));
|
|
498
533
|
// Show where the view went. A spec states an arrangement and not a
|
|
499
534
|
// selection, so `treeFromSpec` shows each cell's FIRST tab — and
|
|
500
535
|
// `newTab` puts the moved view in a tab beside the others, which makes
|
|
@@ -518,7 +553,31 @@ export function WorkspaceLayoutMixin() {
|
|
|
518
553
|
* session spec's layout there is nothing for `orderViews` to apply.
|
|
519
554
|
*/
|
|
520
555
|
tileViews(mode, allViewIds) {
|
|
521
|
-
self.applyLayoutSpec(tileLayoutSpec(allViewIds, mode));
|
|
556
|
+
self.applyLayoutSpec(tileLayoutSpec(liveViewIds(self, allViewIds), mode));
|
|
557
|
+
},
|
|
558
|
+
/**
|
|
559
|
+
* #action
|
|
560
|
+
* Arrange the session's views into panels. Calls `applyLayoutSpec`,
|
|
561
|
+
* turns workspaces mode on for this session (a layout renders nowhere
|
|
562
|
+
* else), and orders `session.views` to the spec's top-to-bottom
|
|
563
|
+
* order, which tabs read their order from. Without the last two steps
|
|
564
|
+
* the layout applies but does not display, or the tabs appear in the
|
|
565
|
+
* wrong order. Leaves take view ids or indexes into `session.views`; a
|
|
566
|
+
* layout that seats no view throws rather than leaving a blank tab. A
|
|
567
|
+
* session spec's `layout` and an agent's live re-layout both call it.
|
|
568
|
+
*/
|
|
569
|
+
layoutViews(spec) {
|
|
570
|
+
const host = self;
|
|
571
|
+
if (!host.setUseWorkspaces || !host.orderViews) {
|
|
572
|
+
throw new Error('This session has no view list to arrange: layoutViews needs a host composed with MultipleViewsSessionMixin');
|
|
573
|
+
}
|
|
574
|
+
host.setUseWorkspaces(true);
|
|
575
|
+
const ids = self.applyLayoutSpec(spec);
|
|
576
|
+
if (ids.length === 0) {
|
|
577
|
+
throw new Error('The layout seats no views: a leaf names its views with "views" (view ids, or indexes into session.views) and a container nests "children"');
|
|
578
|
+
}
|
|
579
|
+
host.orderViews(ids);
|
|
580
|
+
return ids;
|
|
522
581
|
},
|
|
523
582
|
})));
|
|
524
583
|
}
|
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
import type { LayoutTree, NodeKind } from './tree.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A view named by a layout leaf: an index into the view list the layout is
|
|
4
|
+
* applied against (a session spec's own `views`, or `session.views` for the
|
|
5
|
+
* live `applyLayoutSpec`), or a view id.
|
|
6
|
+
*/
|
|
7
|
+
export type LayoutViewRef = number | string;
|
|
2
8
|
/**
|
|
3
9
|
* The `layout` a session spec / URL param states, in the vocabulary users
|
|
4
|
-
* write it in
|
|
5
|
-
* so
|
|
6
|
-
*
|
|
10
|
+
* write it in, and the same shape the live `applyLayoutSpec` action takes — one
|
|
11
|
+
* shape, so an agent that has written one has written the other. **This shape
|
|
12
|
+
* is public** — it is documented as a URL parameter — so it keeps
|
|
13
|
+
* `horizontal`/`vertical`/`tabs` and percentage `size` rather than being
|
|
14
|
+
* renamed to match the internal tree.
|
|
15
|
+
*
|
|
16
|
+
* `resolveLayoutSpec` turns the indexes into ids; the tree builder below takes
|
|
17
|
+
* the resolved form only.
|
|
7
18
|
*/
|
|
8
|
-
export interface LayoutSpecNode {
|
|
19
|
+
export interface LayoutSpecNode<View = LayoutViewRef> {
|
|
9
20
|
/** a leaf: the views to stack vertically in one tab */
|
|
10
|
-
|
|
21
|
+
views?: View[];
|
|
11
22
|
/** how children divide the space; `tabs` puts them in one cell as tabs */
|
|
12
23
|
direction?: 'horizontal' | 'vertical' | 'tabs';
|
|
13
|
-
children?: LayoutSpecNode[];
|
|
24
|
+
children?: LayoutSpecNode<View>[];
|
|
14
25
|
/** share of the parent, as a percentage */
|
|
15
26
|
size?: number;
|
|
16
27
|
}
|
|
28
|
+
export type ResolvedLayoutSpecNode = LayoutSpecNode<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve every view a spec names to an id against `allViewIds`, the list its
|
|
31
|
+
* indexes count into. Anything that resolves to nothing throws, naming what was
|
|
32
|
+
* received: an untyped caller (the MCP `run_javascript` tool) that writes
|
|
33
|
+
* `viewIds` where `views` goes, or an index past the end, used to be accepted
|
|
34
|
+
* as an empty leaf and collapse the workspace into one blank tab with nothing
|
|
35
|
+
* said.
|
|
36
|
+
*
|
|
37
|
+
* A node stating no `views` and no `children` is NOT that slip — it is the
|
|
38
|
+
* empty panel `treeFromSpec` has always built, and refusing it here made the
|
|
39
|
+
* two surfaces disagree about the one shape they share. The slip is an
|
|
40
|
+
* unrecognized KEY, which is what `viewIds` actually is, and naming it beats
|
|
41
|
+
* the old message that could only say the node needed something else.
|
|
42
|
+
*
|
|
43
|
+
* `allViewIds` is undefined only on a model that composes no view list, where
|
|
44
|
+
* an id cannot be checked and an index cannot mean anything.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveLayoutSpec(spec: LayoutSpecNode, allViewIds: string[] | undefined): ResolvedLayoutSpecNode;
|
|
17
47
|
/** A request to move one view relative to the others. Public plugin API. */
|
|
18
48
|
export interface PendingMove {
|
|
19
49
|
type: 'newTab' | 'splitRight';
|
|
@@ -29,9 +59,9 @@ export interface PendingMove {
|
|
|
29
59
|
* nesting, so `size` applies wherever it is written and there is no all-or-
|
|
30
60
|
* nothing pass.
|
|
31
61
|
*/
|
|
32
|
-
export declare function treeFromSpec(spec:
|
|
62
|
+
export declare function treeFromSpec(spec: ResolvedLayoutSpecNode, nextId: (kind: NodeKind) => string): LayoutTree;
|
|
33
63
|
/** Every viewId a spec names, depth-first — the order it states. */
|
|
34
|
-
export declare function viewIdsInSpec(spec:
|
|
64
|
+
export declare function viewIdsInSpec(spec: ResolvedLayoutSpecNode): string[];
|
|
35
65
|
/** How "arrange everything" lays the whole session out. */
|
|
36
66
|
export type TileMode = 'tabs' | 'horizontal' | 'vertical' | 'grid';
|
|
37
67
|
/**
|
|
@@ -49,10 +79,10 @@ export type TileMode = 'tabs' | 'horizontal' | 'vertical' | 'grid';
|
|
|
49
79
|
* width, since `resolveSizes` reads bare siblings as an even share of their own
|
|
50
80
|
* branch rather than of the grid.
|
|
51
81
|
*/
|
|
52
|
-
export declare function tileLayoutSpec(viewIds: string[], mode: TileMode):
|
|
82
|
+
export declare function tileLayoutSpec(viewIds: string[], mode: TileMode): ResolvedLayoutSpecNode;
|
|
53
83
|
/**
|
|
54
84
|
* `setPendingMove` as a spec: everything else keeps its side, the named view
|
|
55
85
|
* takes the other. With nothing else on screen there is nothing to split from,
|
|
56
86
|
* so the view just takes the space.
|
|
57
87
|
*/
|
|
58
|
-
export declare function specForPendingMove(move: PendingMove, allViewIds: string[]):
|
|
88
|
+
export declare function specForPendingMove(move: PendingMove, allViewIds: string[]): ResolvedLayoutSpecNode;
|
|
@@ -1,4 +1,77 @@
|
|
|
1
1
|
import { normalize } from './tree.js';
|
|
2
|
+
// `views`/`children` are pulled off by name; the rest pass through to the tree,
|
|
3
|
+
// so an unrecognized one is a key the writer expected to mean something
|
|
4
|
+
const layoutNodeKeys = new Set(['views', 'direction', 'children', 'size']);
|
|
5
|
+
/**
|
|
6
|
+
* Resolve every view a spec names to an id against `allViewIds`, the list its
|
|
7
|
+
* indexes count into. Anything that resolves to nothing throws, naming what was
|
|
8
|
+
* received: an untyped caller (the MCP `run_javascript` tool) that writes
|
|
9
|
+
* `viewIds` where `views` goes, or an index past the end, used to be accepted
|
|
10
|
+
* as an empty leaf and collapse the workspace into one blank tab with nothing
|
|
11
|
+
* said.
|
|
12
|
+
*
|
|
13
|
+
* A node stating no `views` and no `children` is NOT that slip — it is the
|
|
14
|
+
* empty panel `treeFromSpec` has always built, and refusing it here made the
|
|
15
|
+
* two surfaces disagree about the one shape they share. The slip is an
|
|
16
|
+
* unrecognized KEY, which is what `viewIds` actually is, and naming it beats
|
|
17
|
+
* the old message that could only say the node needed something else.
|
|
18
|
+
*
|
|
19
|
+
* `allViewIds` is undefined only on a model that composes no view list, where
|
|
20
|
+
* an id cannot be checked and an index cannot mean anything.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveLayoutSpec(spec, allViewIds) {
|
|
23
|
+
const describe = (value) => typeof value === 'string' ? `"${value}"` : String(value);
|
|
24
|
+
const resolveRef = (ref) => {
|
|
25
|
+
if (typeof ref === 'number') {
|
|
26
|
+
if (!allViewIds) {
|
|
27
|
+
throw new Error(`Layout names view index ${ref}, but this session has no view list for an index to count into; name the view by id`);
|
|
28
|
+
}
|
|
29
|
+
const id = allViewIds[ref];
|
|
30
|
+
if (id === undefined) {
|
|
31
|
+
throw new Error(`Layout names view index ${ref}, but the session has ${allViewIds.length} view(s)${allViewIds.length ? ` (indexes 0-${allViewIds.length - 1})` : ''}`);
|
|
32
|
+
}
|
|
33
|
+
return id;
|
|
34
|
+
}
|
|
35
|
+
if (typeof ref !== 'string') {
|
|
36
|
+
throw new Error(`Layout "views" entries are view indexes or view ids; received ${describe(ref)}`);
|
|
37
|
+
}
|
|
38
|
+
if (allViewIds && !allViewIds.includes(ref)) {
|
|
39
|
+
throw new Error(`Layout names view id ${describe(ref)}, which is not a view in this session (ids: ${allViewIds.map(describe).join(', ') || 'none'})`);
|
|
40
|
+
}
|
|
41
|
+
return ref;
|
|
42
|
+
};
|
|
43
|
+
const resolveNode = (node) => {
|
|
44
|
+
const { views, children, ...rest } = node;
|
|
45
|
+
const unknown = Object.keys(rest).filter(key => !layoutNodeKeys.has(key));
|
|
46
|
+
if (unknown.length > 0) {
|
|
47
|
+
throw new Error(`Layout node has unrecognized key(s) ${unknown.map(describe).join(', ')}; a leaf names its views with "views" (view indexes or ids) and a container nests "children"`);
|
|
48
|
+
}
|
|
49
|
+
if (views !== undefined && !Array.isArray(views)) {
|
|
50
|
+
throw new Error(`Layout "views" is an array of view indexes or ids; received ${describe(views)}`);
|
|
51
|
+
}
|
|
52
|
+
if (children !== undefined && !Array.isArray(children)) {
|
|
53
|
+
throw new Error(`Layout "children" is an array of layout nodes; received ${describe(children)}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
...rest,
|
|
57
|
+
...(views === undefined ? {} : { views: views.map(resolveRef) }),
|
|
58
|
+
...(children === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: { children: children.map(resolveNode) }),
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const resolved = resolveNode(spec);
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const repeated = new Set(viewIdsInSpec(resolved).filter(id => {
|
|
66
|
+
const already = seen.has(id);
|
|
67
|
+
seen.add(id);
|
|
68
|
+
return already;
|
|
69
|
+
}));
|
|
70
|
+
if (repeated.size > 0) {
|
|
71
|
+
throw new Error(`Layout seats view ${[...repeated].map(describe).join(', ')} in more than one cell; a view lives in exactly one tab, so name it once`);
|
|
72
|
+
}
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
2
75
|
/**
|
|
3
76
|
* The effective size of each child of one branch.
|
|
4
77
|
*
|
|
@@ -44,11 +117,11 @@ function resolveSizes(children) {
|
|
|
44
117
|
*/
|
|
45
118
|
export function treeFromSpec(spec, nextId) {
|
|
46
119
|
function build(node, size) {
|
|
47
|
-
if (node.
|
|
120
|
+
if (node.views) {
|
|
48
121
|
return {
|
|
49
122
|
id: nextId('panel'),
|
|
50
123
|
size,
|
|
51
|
-
tabs: [{ id: nextId('tab'), viewIds: [...node.
|
|
124
|
+
tabs: [{ id: nextId('tab'), viewIds: [...node.views] }],
|
|
52
125
|
activeTabId: undefined,
|
|
53
126
|
};
|
|
54
127
|
}
|
|
@@ -67,7 +140,7 @@ export function treeFromSpec(spec, nextId) {
|
|
|
67
140
|
if (node.direction === 'tabs') {
|
|
68
141
|
const tabs = children.flatMap(child => {
|
|
69
142
|
const viewIds = viewIdsInSpec(child);
|
|
70
|
-
return child.
|
|
143
|
+
return child.views === undefined && viewIds.length === 0
|
|
71
144
|
? []
|
|
72
145
|
: [{ id: nextId('tab'), viewIds }];
|
|
73
146
|
});
|
|
@@ -98,7 +171,7 @@ export function treeFromSpec(spec, nextId) {
|
|
|
98
171
|
/** Every viewId a spec names, depth-first — the order it states. */
|
|
99
172
|
export function viewIdsInSpec(spec) {
|
|
100
173
|
return [
|
|
101
|
-
...(spec.
|
|
174
|
+
...(spec.views ?? []),
|
|
102
175
|
...(spec.children ?? []).flatMap(viewIdsInSpec),
|
|
103
176
|
];
|
|
104
177
|
}
|
|
@@ -121,9 +194,9 @@ export function tileLayoutSpec(viewIds, mode) {
|
|
|
121
194
|
// Nothing to arrange: one view is the whole workspace whatever the mode, and
|
|
122
195
|
// no views leaves the empty panel the tree already guarantees.
|
|
123
196
|
if (viewIds.length <= 1) {
|
|
124
|
-
return {
|
|
197
|
+
return { views: [...viewIds] };
|
|
125
198
|
}
|
|
126
|
-
const cell = (id) => ({
|
|
199
|
+
const cell = (id) => ({ views: [id] });
|
|
127
200
|
if (mode !== 'grid') {
|
|
128
201
|
return { direction: mode, children: viewIds.map(cell) };
|
|
129
202
|
}
|
|
@@ -147,7 +220,7 @@ export function specForPendingMove(move, allViewIds) {
|
|
|
147
220
|
return others.length > 0
|
|
148
221
|
? {
|
|
149
222
|
direction: move.type === 'splitRight' ? 'horizontal' : 'tabs',
|
|
150
|
-
children: [{
|
|
223
|
+
children: [{ views: others }, { views: [move.viewId] }],
|
|
151
224
|
}
|
|
152
|
-
: {
|
|
225
|
+
: { views: [move.viewId] };
|
|
153
226
|
}
|
package/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { App,
|
|
1
|
+
export { App, DialogQueue, preloadAppFrame, preloadAppShell, } from './ui/index.ts';
|
|
2
2
|
export { HistoryManagementMixin } from './HistoryManagement/index.ts';
|
|
3
3
|
export { DEFAULT_SHARE_URL, JBrowseConfigF } from './JBrowseConfig/index.ts';
|
|
4
4
|
export { JBrowseModelF } from './JBrowseModel/index.ts';
|
|
@@ -13,4 +13,6 @@ export type { LayoutNode, LgvUrlInit, ParsedSessionSpec, ViewSpec, } from './Ses
|
|
|
13
13
|
export type { SessionModelFactory } from './sessionModelFactory.ts';
|
|
14
14
|
export { deleteQueryParams, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.ts';
|
|
15
15
|
export { processMutableMenuActions, resolveMenus } from './menus.ts';
|
|
16
|
-
export type { Menu, MenuAction
|
|
16
|
+
export type { Menu, MenuAction } from './menus.ts';
|
|
17
|
+
export { createJbApi, ensureReExports, safeJson, sessionOf, undeliveredNotifications, waitReady, } from './JbApi/index.ts';
|
|
18
|
+
export type { JbApi } from './JbApi/index.ts';
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { App,
|
|
1
|
+
export { App, DialogQueue, preloadAppFrame, preloadAppShell, } from './ui/index.js';
|
|
2
2
|
export { HistoryManagementMixin } from './HistoryManagement/index.js';
|
|
3
3
|
export { DEFAULT_SHARE_URL, JBrowseConfigF } from './JBrowseConfig/index.js';
|
|
4
4
|
export { JBrowseModelF } from './JBrowseModel/index.js';
|
|
@@ -11,3 +11,6 @@ export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSp
|
|
|
11
11
|
// embedded product must not rewrite its host's URL — see queryParams.ts
|
|
12
12
|
export { deleteQueryParams, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.js';
|
|
13
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';
|