@jbrowse/product-core 4.3.0 → 5.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/RootModel/BaseRootModel.d.ts +629 -304
- package/esm/RootModel/BaseRootModel.js +138 -14
- package/esm/RootModel/HierarchicalConfig.d.ts +57 -21
- package/esm/RootModel/HierarchicalConfig.js +36 -0
- package/esm/RootModel/InternetAccounts.d.ts +18 -1
- package/esm/RootModel/InternetAccounts.js +120 -22
- package/esm/RootModel/PreferencesConfig.d.ts +53 -0
- package/esm/RootModel/PreferencesConfig.js +61 -0
- package/esm/RootModel/createConfigModel.d.ts +232 -0
- package/esm/RootModel/createConfigModel.js +56 -0
- package/esm/RootModel/index.d.ts +10 -5
- package/esm/RootModel/index.js +10 -5
- package/esm/RootModel/menuItems.d.ts +24 -0
- package/esm/RootModel/menuItems.js +143 -0
- package/esm/Session/BaseSession.d.ts +310 -11
- package/esm/Session/BaseSession.js +448 -14
- package/esm/Session/Connections.d.ts +114 -8
- package/esm/Session/Connections.js +195 -74
- package/esm/Session/DrawerWidgets.d.ts +115 -12
- package/esm/Session/DrawerWidgets.js +161 -42
- package/esm/Session/MultipleViews.d.ts +1266 -1128
- package/esm/Session/MultipleViews.js +357 -66
- package/esm/Session/Preferences.d.ts +1665 -0
- package/esm/Session/Preferences.js +54 -0
- package/esm/Session/ReferenceManagement.d.ts +28 -3
- package/esm/Session/ReferenceManagement.js +56 -41
- package/esm/Session/SessionTracks.d.ts +1275 -1138
- package/esm/Session/SessionTracks.js +532 -35
- package/esm/Session/Themes.d.ts +87 -2
- package/esm/Session/Themes.js +156 -23
- package/esm/Session/TrackMenu.d.ts +112 -0
- package/esm/Session/TrackMenu.js +200 -0
- package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
- package/esm/Session/TrackMenuSessionMixin.js +41 -0
- package/esm/Session/Tracks.d.ts +1169 -1117
- package/esm/Session/Tracks.js +160 -20
- package/esm/Session/assertSessionModel.d.ts +21 -0
- package/esm/Session/assertSessionModel.js +1 -0
- package/esm/Session/finalizeSession.d.ts +16 -0
- package/esm/Session/finalizeSession.js +29 -0
- package/esm/Session/index.d.ts +22 -10
- package/esm/Session/index.js +13 -10
- package/esm/Session/shareableSnapshot.d.ts +77 -0
- package/esm/Session/shareableSnapshot.js +196 -0
- package/esm/Session/temporaryAssemblyTracks.d.ts +23 -0
- package/esm/Session/temporaryAssemblyTracks.js +38 -0
- package/esm/assertExtends.d.ts +55 -0
- package/esm/assertExtends.js +1 -0
- package/esm/controllerTracks.d.ts +56 -0
- package/esm/controllerTracks.js +77 -0
- package/esm/destroyViewState.d.ts +21 -0
- package/esm/destroyViewState.js +29 -0
- package/esm/index.d.ts +31 -4
- package/esm/index.js +31 -4
- package/esm/loadPlugins.d.ts +33 -0
- package/esm/loadPlugins.js +38 -0
- package/esm/localFiles.d.ts +74 -0
- package/esm/localFiles.js +156 -0
- package/esm/observeSession.d.ts +56 -0
- package/esm/observeSession.js +91 -0
- package/esm/pluginInput.d.ts +32 -0
- package/esm/pluginInput.js +19 -0
- package/esm/pruneUnbuildableNodes.d.ts +109 -0
- package/esm/pruneUnbuildableNodes.js +568 -0
- package/esm/resolveAssemblies.d.ts +44 -0
- package/esm/resolveAssemblies.js +55 -0
- package/esm/rpcWorker.d.ts +26 -0
- package/esm/rpcWorker.js +96 -15
- package/esm/scheduleDetachedDestroy.d.ts +18 -0
- package/esm/scheduleDetachedDestroy.js +24 -0
- package/esm/sessionMigrations/index.d.ts +53 -0
- package/esm/sessionMigrations/index.js +432 -0
- package/esm/sessionUrl.d.ts +41 -0
- package/esm/sessionUrl.js +53 -0
- package/esm/sessionUtils.d.ts +69 -0
- package/esm/sessionUtils.js +613 -0
- package/esm/siblingCast.d.ts +14 -0
- package/esm/siblingCast.js +16 -0
- package/esm/snapshotUtils.d.ts +8 -0
- package/esm/snapshotUtils.js +12 -0
- package/esm/ui/AboutDialog.d.ts +3 -6
- package/esm/ui/AboutDialog.js +4 -4
- package/esm/ui/AboutDialogContents.d.ts +2 -6
- package/esm/ui/AboutDialogContents.js +34 -13
- package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
- package/esm/ui/AssemblyInfoPanel.js +69 -0
- package/esm/ui/DisplayDefaultsSection.d.ts +36 -0
- package/esm/ui/DisplayDefaultsSection.js +65 -0
- package/esm/ui/Drawer.d.ts +21 -0
- package/esm/ui/Drawer.js +60 -0
- package/esm/ui/DrawerControls.d.ts +5 -0
- package/esm/ui/DrawerControls.js +28 -0
- package/esm/ui/DrawerHeader.d.ts +6 -0
- package/esm/ui/DrawerHeader.js +36 -0
- package/esm/ui/DrawerWidget.d.ts +5 -0
- package/esm/ui/DrawerWidget.js +12 -0
- package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
- package/esm/ui/DrawerWidgetSelector.js +40 -0
- package/esm/ui/FileInfoPanel.d.ts +3 -6
- package/esm/ui/FileInfoPanel.js +35 -26
- package/esm/ui/HeaderButtons.d.ts +7 -1
- package/esm/ui/HeaderButtons.js +23 -18
- package/esm/ui/ModalWidget.d.ts +12 -0
- package/esm/ui/ModalWidget.js +33 -0
- package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
- package/esm/ui/ModalWidgetAppBar.js +9 -0
- package/esm/ui/PreferencesDialog.d.ts +56 -0
- package/esm/ui/PreferencesDialog.js +145 -0
- package/esm/ui/PreferencesResetDialog.d.ts +8 -0
- package/esm/ui/PreferencesResetDialog.js +19 -0
- package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
- package/esm/ui/RefNameAliasesDialog.js +65 -0
- package/esm/ui/RefNameInfoDialog.d.ts +3 -6
- package/esm/ui/RefNameInfoDialog.js +57 -63
- package/esm/ui/WidgetBody.d.ts +18 -0
- package/esm/ui/WidgetBody.js +22 -0
- package/esm/ui/WidgetHeading.d.ts +7 -0
- package/esm/ui/WidgetHeading.js +7 -0
- package/esm/ui/drawerLayout.d.ts +18 -0
- package/esm/ui/drawerLayout.js +22 -0
- package/esm/ui/index.d.ts +13 -1
- package/esm/ui/index.js +12 -1
- package/esm/ui/util.d.ts +37 -4
- package/esm/ui/util.js +35 -38
- package/esm/useEngineLifecycle.d.ts +13 -0
- package/esm/useEngineLifecycle.js +22 -0
- package/package.json +11 -15
- package/esm/RootModel/FormatAbout.d.ts +0 -12
- package/esm/RootModel/FormatAbout.js +0 -15
- package/esm/RootModel/FormatDetails.d.ts +0 -24
- package/esm/RootModel/FormatDetails.js +0 -27
- package/esm/Session/DialogQueue.d.ts +0 -17
- package/esm/Session/DialogQueue.js +0 -31
|
@@ -1,88 +1,379 @@
|
|
|
1
|
-
import { localStorageGetBoolean, localStorageSetBoolean, } from '@jbrowse/core/util';
|
|
2
|
-
import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree';
|
|
1
|
+
import { applyOrderWithin, localStorageGetBoolean, localStorageSetBoolean, reorderWithin, } from '@jbrowse/core/util';
|
|
2
|
+
import { addDisposer, applyPatch, cast, detach, getMembers, getParent, hasParent, isReferenceType, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
|
|
3
3
|
import { autorun } from 'mobx';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { scheduleDetachedDestroy } from '../scheduleDetachedDestroy.js';
|
|
5
|
+
import { BaseSessionModel, isBaseSession } from './BaseSession.js';
|
|
6
|
+
import { DrawerWidgetSessionMixin } from './DrawerWidgets.js';
|
|
7
|
+
// Whether `node` IS `view` or lives somewhere under it. A view can CONTAIN
|
|
8
|
+
// views, so `takeOut` matches widgets to the view leaving by containment rather
|
|
9
|
+
// than by `widget.view.id === view.id`. ADR-069 says what that cost.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately not `findParentThat`, which warns and throws when the walk runs
|
|
12
|
+
// out: this asks the question of every reference every widget holds, and most
|
|
13
|
+
// of them point somewhere else entirely, so "no" is an ordinary answer here
|
|
14
|
+
// rather than a fault.
|
|
15
|
+
function isWithin(node, view) {
|
|
16
|
+
if (!isStateTreeNode(node)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
let cur = node;
|
|
20
|
+
while (cur !== view) {
|
|
21
|
+
if (!hasParent(cur)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
cur = getParent(cur);
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function viewIdsWithin(view) {
|
|
29
|
+
return [view.id, ...(view.views ?? []).flatMap(v => viewIdsWithin(v))];
|
|
30
|
+
}
|
|
31
|
+
// Empty every reference `holder` has into `view`, and say whether it had any.
|
|
32
|
+
//
|
|
33
|
+
// MST invalidates a reference when its TARGET node is detached or destroyed, so
|
|
34
|
+
// a widget's reference to the view itself empties on its way out and one
|
|
35
|
+
// pointing INSIDE it does not: `openFeatureWidget` stores the clicked feature's
|
|
36
|
+
// track, and that node is neither detached nor destroyed — it just leaves the
|
|
37
|
+
// session's identifier cache with the view, which makes reading the reference
|
|
38
|
+
// THROW where `safeReference` promises undefined. The widget's own autoruns go
|
|
39
|
+
// on running whether or not its panel is on screen, so that throw comes out of
|
|
40
|
+
// a reaction, uncatchable by the caller.
|
|
41
|
+
//
|
|
42
|
+
// So do here what `onInvalidated` would have done, while the reference can
|
|
43
|
+
// still be read. Only where the type allows undefined — a required reference
|
|
44
|
+
// has no empty value to move to, and the widget is hidden either way. ADR-069.
|
|
45
|
+
function dropRefsInto(holder, view) {
|
|
46
|
+
let found = false;
|
|
47
|
+
for (const [key, prop] of Object.entries(getMembers(holder).properties)) {
|
|
48
|
+
if (isReferenceType(prop) && isWithin(holder[key], view)) {
|
|
49
|
+
found = true;
|
|
50
|
+
if (prop.is(undefined)) {
|
|
51
|
+
applyPatch(holder, { op: 'replace', path: `/${key}`, value: undefined });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* #stateModel MultipleViewsSessionMixin
|
|
59
|
+
*/
|
|
6
60
|
export function MultipleViewsSessionMixin(pluginManager) {
|
|
7
61
|
return types
|
|
8
62
|
.compose(BaseSessionModel(pluginManager), DrawerWidgetSessionMixin(pluginManager), types.model({
|
|
63
|
+
/**
|
|
64
|
+
* #property
|
|
65
|
+
*/
|
|
9
66
|
views: types.array(pluginManager.pluggableMstType('view', 'stateModel')),
|
|
67
|
+
/**
|
|
68
|
+
* #property
|
|
69
|
+
*/
|
|
10
70
|
stickyViewHeaders: types.optional(types.boolean, () => localStorageGetBoolean('stickyViewHeaders', true)),
|
|
11
|
-
|
|
71
|
+
/**
|
|
72
|
+
* #property
|
|
73
|
+
* enables the tabbed/tiled workspace layout for this session. Undefined
|
|
74
|
+
* means unspecified — read `effectiveUseWorkspaces`.
|
|
75
|
+
*/
|
|
76
|
+
useWorkspaces: types.stripDefault(types.maybe(types.boolean), undefined),
|
|
12
77
|
}))
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
moveViewToTop(id) {
|
|
27
|
-
const idx = self.views.findIndex(view => view.id === id);
|
|
28
|
-
self.views = cast([
|
|
29
|
-
self.views[idx],
|
|
30
|
-
...self.views.filter(view => view.id !== id),
|
|
31
|
-
]);
|
|
78
|
+
.views(self => ({
|
|
79
|
+
/**
|
|
80
|
+
* #getter
|
|
81
|
+
* resolved workspaces flag (never undefined): this session's value, else
|
|
82
|
+
* the user preference over the `configuration.preferences.useWorkspaces`
|
|
83
|
+
* admin default. Every consumer reads this, not the raw property — only
|
|
84
|
+
* sessions built from a snapshot or a spec `layout` set that, so the
|
|
85
|
+
* admin default is what reaches the arrivals that bypass defaultSession.
|
|
86
|
+
*/
|
|
87
|
+
get effectiveUseWorkspaces() {
|
|
88
|
+
return (self.useWorkspaces ?? self.getPreference('useWorkspaces') === true);
|
|
32
89
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
90
|
+
/**
|
|
91
|
+
* #getter
|
|
92
|
+
* what `effectiveUseWorkspaces` becomes after `resetUseWorkspaces` — the
|
|
93
|
+
* admin default, with both this session's own value and the user's
|
|
94
|
+
* override out of the way. The Preferences reset diff needs this rather
|
|
95
|
+
* than the override map, which can't see a session-scoped value (a spec
|
|
96
|
+
* `layout`, a "move view to a tab") and so reported nothing to reset.
|
|
97
|
+
*/
|
|
98
|
+
get defaultUseWorkspaces() {
|
|
99
|
+
return self.getPreferenceDefault('useWorkspaces') === true;
|
|
39
100
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
101
|
+
}))
|
|
102
|
+
.actions(self => {
|
|
103
|
+
// `scopeIds` narrows the move to a subset of the stack: in a tabbed
|
|
104
|
+
// workspace "move this view up" means up past the previous view IN THIS
|
|
105
|
+
// PANEL, and the view that happens to precede it in `self.views` may be
|
|
106
|
+
// in another panel, where moving past it would look like a no-op. Omitted
|
|
107
|
+
// (the classic stack) every view is in scope and this is a plain reorder.
|
|
108
|
+
const move = (id, direction, scopeIds) => {
|
|
109
|
+
const idx = self.views.findIndex(v => v.id === id);
|
|
110
|
+
const scope = scopeIds && new Set(scopeIds);
|
|
111
|
+
self.views = cast(reorderWithin(self.views, idx, direction, v => scope ? scope.has(v.id) : true));
|
|
112
|
+
};
|
|
113
|
+
// The body of removeView, callable from replaceView below — an action
|
|
114
|
+
// can't reach a sibling action through `self` from inside the same
|
|
115
|
+
// .actions() block, since neither is attached yet.
|
|
116
|
+
//
|
|
117
|
+
// **Detached, not removed.** `views.remove` destroys the view and
|
|
118
|
+
// everything under it right here, inside the action, and MobX runs the
|
|
119
|
+
// action's pending reactions at the `endBatch` closing it — so every
|
|
120
|
+
// component still mounted over that view gets a final run against nodes
|
|
121
|
+
// that just died. ADR-069.
|
|
122
|
+
//
|
|
123
|
+
// A view is the worse half of that rule rather than the `setSession`
|
|
124
|
+
// half, because what is mounted over it is a DISPLAY, and a display's
|
|
125
|
+
// reads reach `getContainingView`, which walks parents and THROWS when
|
|
126
|
+
// there is none. So this was not only a noisy console: on
|
|
127
|
+
// `cancer_sv/multihop_split_view` the throw went to an ErrorBoundary with
|
|
128
|
+
// no view left under it and took the page. Detaching leaves the walk
|
|
129
|
+
// something to find.
|
|
130
|
+
const takeOut = (view) => {
|
|
131
|
+
// Membership first, and before anything reads through `view`. A view
|
|
132
|
+
// already out of the session reaches here — `replaceView` documents
|
|
133
|
+
// that case below — and by then it is detached, or destroyed by the
|
|
134
|
+
// task scheduled at the bottom. MST's `detach` throws on a node with no
|
|
135
|
+
// parent, and the widgets holding it were emptied when it left.
|
|
136
|
+
if (!self.views.includes(view)) {
|
|
137
|
+
return;
|
|
44
138
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
139
|
+
// The focus is a persisted id, not a reference, so nothing drops it
|
|
140
|
+
// when the view it names goes. Every consumer compares
|
|
141
|
+
// `focusedViewId === view.id`, so a stale one matches nothing — the
|
|
142
|
+
// focus ring vanishes and the drawer stops naming a view, with nothing
|
|
143
|
+
// to say why — and it ships in a saved or shared session. `replaceView`
|
|
144
|
+
// captures `wasFocused` above this and re-points the focus at the
|
|
145
|
+
// replacement afterwards, which is the one case where the focus moves
|
|
146
|
+
// rather than ending.
|
|
147
|
+
const { focusedViewId } = self;
|
|
148
|
+
if (focusedViewId !== undefined &&
|
|
149
|
+
viewIdsWithin(view).includes(focusedViewId)) {
|
|
150
|
+
self.setFocusedViewId(undefined);
|
|
151
|
+
}
|
|
152
|
+
// Every widget, not the active ones: a widget the user closed keeps
|
|
153
|
+
// its references and its autoruns. One that pointed into the view is
|
|
154
|
+
// now empty, so hide it rather than leave the panel showing what it no
|
|
155
|
+
// longer has.
|
|
156
|
+
for (const [, widget] of self.widgets) {
|
|
157
|
+
if (dropRefsInto(widget, view)) {
|
|
54
158
|
self.hideWidget(widget);
|
|
55
159
|
}
|
|
56
160
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
161
|
+
// MST fires the view's `beforeDetach` here, while it is still attached
|
|
162
|
+
// and the walk to the session still resolves. That matters: anything of
|
|
163
|
+
// a view's that reaches OUTSIDE its own tree has to move to that hook,
|
|
164
|
+
// because after this line the view is a root and `getSession` from a
|
|
165
|
+
// root throws. ADR-069's detach-time disposer, at view scope — the
|
|
166
|
+
// comparative views' `releaseTemporaryAssemblies` is the one case.
|
|
167
|
+
detach(view);
|
|
168
|
+
scheduleDetachedDestroy(view);
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
/**
|
|
172
|
+
* #action
|
|
173
|
+
*/
|
|
174
|
+
moveViewDown(id, scopeIds) {
|
|
175
|
+
move(id, 'down', scopeIds);
|
|
176
|
+
},
|
|
177
|
+
/**
|
|
178
|
+
* #action
|
|
179
|
+
*/
|
|
180
|
+
moveViewUp(id, scopeIds) {
|
|
181
|
+
move(id, 'up', scopeIds);
|
|
182
|
+
},
|
|
183
|
+
/**
|
|
184
|
+
* #action
|
|
185
|
+
*/
|
|
186
|
+
moveViewToTop(id, scopeIds) {
|
|
187
|
+
move(id, 'top', scopeIds);
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* #action
|
|
191
|
+
*/
|
|
192
|
+
moveViewToBottom(id, scopeIds) {
|
|
193
|
+
move(id, 'bottom', scopeIds);
|
|
194
|
+
},
|
|
195
|
+
/**
|
|
196
|
+
* #action
|
|
197
|
+
* Put the named views into the given relative order, leaving views not
|
|
198
|
+
* named in their own slots.
|
|
199
|
+
*
|
|
200
|
+
* `session.views` is the one ordering, so this is how a channel that
|
|
201
|
+
* states an order in some other vocabulary gets it applied: a session
|
|
202
|
+
* spec's `layout` names views per panel, top to bottom, and that used to
|
|
203
|
+
* be honoured by the panel assignment array's order. Now the assignment
|
|
204
|
+
* carries membership only, so the layout says it here instead, once.
|
|
205
|
+
*/
|
|
206
|
+
orderViews(ids) {
|
|
207
|
+
self.views = cast(applyOrderWithin(self.views, ids, v => v.id));
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* #action
|
|
211
|
+
*/
|
|
212
|
+
addView(typeName, initialState = {}) {
|
|
213
|
+
const length = self.views.push({
|
|
214
|
+
...initialState,
|
|
215
|
+
type: typeName,
|
|
216
|
+
});
|
|
217
|
+
return self.views[length - 1];
|
|
218
|
+
},
|
|
219
|
+
/**
|
|
220
|
+
* #action
|
|
221
|
+
* swap `view` for a new view of `typeName`, in the slot it occupied.
|
|
222
|
+
*
|
|
223
|
+
* The launchers that build a view of a DIFFERENT type out of one you are
|
|
224
|
+
* already looking at — launch synteny view, read-vs-ref, split view —
|
|
225
|
+
* otherwise append, leaving the source view above the thing it produced
|
|
226
|
+
* and a stack of two views showing the same locus. This is the "replace"
|
|
227
|
+
* half of that offer, and a slot swap rather than remove-then-add so the
|
|
228
|
+
* new view lands where the reader was looking instead of at the bottom of
|
|
229
|
+
* the session.
|
|
230
|
+
*
|
|
231
|
+
* A launcher whose result is the SAME type as its source does not come
|
|
232
|
+
* here and should not: collapse introns offers "Replace current view" by
|
|
233
|
+
* navigating the view it was invoked on, which keeps the view's identity
|
|
234
|
+
* and so lets its snackbar offer an Undo. Swapping in a new node would
|
|
235
|
+
* leave nothing to undo onto.
|
|
236
|
+
*
|
|
237
|
+
* The slot is `session.views`, which is the order views render in under
|
|
238
|
+
* both layout modes (a tab's `viewIds` says which tab a view is in, not
|
|
239
|
+
* where in it), so the swap lands in place either way.
|
|
240
|
+
*
|
|
241
|
+
* WHICH panel is a separate question and this does not answer it: the
|
|
242
|
+
* new view arrives in no tab, and `homeUnassignedViews` puts it in the
|
|
243
|
+
* active panel — the same panel only when the replaced view was in the
|
|
244
|
+
* active one. In practice it is, since clicking into a view activates
|
|
245
|
+
* its panel, and the launch that offers a replace is a click on that
|
|
246
|
+
* view's own menu.
|
|
247
|
+
*/
|
|
248
|
+
replaceView(view, typeName, initialState = {}) {
|
|
249
|
+
// read before the removal, which is what makes both stale
|
|
250
|
+
const idx = self.views.indexOf(view);
|
|
251
|
+
// `idx` first, so a view already gone from the session short-circuits
|
|
252
|
+
// before `view.id` is read: that node is detached and, a task later,
|
|
253
|
+
// destroyed — and MST warns on any read through a destroyed one
|
|
254
|
+
const { focusedViewId } = self;
|
|
255
|
+
const wasFocused = idx !== -1 &&
|
|
256
|
+
focusedViewId !== undefined &&
|
|
257
|
+
viewIdsWithin(view).includes(focusedViewId);
|
|
258
|
+
takeOut(view);
|
|
259
|
+
// a view already gone from the session appends, rather than throwing
|
|
260
|
+
// or silently dropping the launch
|
|
261
|
+
const at = idx === -1 ? self.views.length : idx;
|
|
262
|
+
self.views.splice(at, 0, { ...initialState, type: typeName });
|
|
263
|
+
const created = self.views[at];
|
|
264
|
+
// The replacement takes the slot, so it takes the focus with it.
|
|
265
|
+
// Every consumer compares `focusedViewId === view.id`, so a
|
|
266
|
+
// replacement that left the old id in place would simply match
|
|
267
|
+
// nothing: the focus ring would vanish and the drawer would stop
|
|
268
|
+
// naming a view, with nothing to say why.
|
|
269
|
+
if (wasFocused) {
|
|
270
|
+
self.setFocusedViewId(created.id);
|
|
271
|
+
}
|
|
272
|
+
return created;
|
|
273
|
+
},
|
|
274
|
+
/**
|
|
275
|
+
* #action
|
|
276
|
+
*/
|
|
277
|
+
removeView(view) {
|
|
278
|
+
takeOut(view);
|
|
279
|
+
},
|
|
280
|
+
/**
|
|
281
|
+
* #action
|
|
282
|
+
* Take out every view the given session snapshot will not keep, before
|
|
283
|
+
* a whole-tree `applySnapshot` destroys it where it stands.
|
|
284
|
+
*
|
|
285
|
+
* Undo and redo are `applySnapshot` on the session
|
|
286
|
+
* (`core/util/TimeTraveller`, which calls this), and every view carries
|
|
287
|
+
* an `ElementId` — so MST reconciles by identifier and destroys what the
|
|
288
|
+
* target lacks, inside the action, with the components still mounted.
|
|
289
|
+
* Measured on a redo across a closed view, with the restored view
|
|
290
|
+
* repainted first: 4 liveliness reads of its display before this, 0
|
|
291
|
+
* after.
|
|
292
|
+
*
|
|
293
|
+
* A view that is present but has changed `type` is destroyed by that
|
|
294
|
+
* same reconciliation (`areSame` runs the type's `is()` before its id
|
|
295
|
+
* check), so identity here is the pair, not the id.
|
|
296
|
+
*
|
|
297
|
+
* **Views, and deliberately not what is under them.** A track or a
|
|
298
|
+
* display the snapshot drops is still destroyed in place, and that is
|
|
299
|
+
* not this door being worse than the others: `hideTrackGeneric` is a
|
|
300
|
+
* plain `tracks.remove` and measures louder (5 reads to undo's 4).
|
|
301
|
+
* Detaching one would be worse than the destroy it replaced — a
|
|
302
|
+
* detached display is a LIVE root, and `getContainingView` walks
|
|
303
|
+
* parents and throws when the walk finds no view, where a dead node
|
|
304
|
+
* only warns. At or above the view is the only place that walk still
|
|
305
|
+
* lands, which is why ADR-069's rule stops there.
|
|
306
|
+
*/
|
|
307
|
+
takeOutViewsMissingFrom(snapshot) {
|
|
308
|
+
const incoming = snapshot?.views ?? [];
|
|
309
|
+
const kept = new Map(incoming.map(v => [v.id, v.type]));
|
|
310
|
+
// a copy, because `takeOut` splices the array it is iterating
|
|
311
|
+
for (const view of [...self.views]) {
|
|
312
|
+
if (kept.get(view.id) !== view.type) {
|
|
313
|
+
takeOut(view);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* #action
|
|
319
|
+
*/
|
|
320
|
+
setStickyViewHeaders(sticky) {
|
|
321
|
+
self.stickyViewHeaders = sticky;
|
|
322
|
+
},
|
|
323
|
+
/**
|
|
324
|
+
* #action
|
|
325
|
+
* set the workspaces layout for this session only, leaving the user's
|
|
326
|
+
* personal default untouched. For session-scoped intent — a spec
|
|
327
|
+
* carrying a `layout`, or an ad-hoc "move view to a tab/split" — where
|
|
328
|
+
* rewriting the visitor's global preference would be a surprise. The
|
|
329
|
+
* user-facing default toggle is `setUseWorkspacesPreference`.
|
|
330
|
+
*/
|
|
331
|
+
setUseWorkspaces(useWorkspaces) {
|
|
332
|
+
self.useWorkspaces = useWorkspaces;
|
|
333
|
+
},
|
|
334
|
+
/**
|
|
335
|
+
* #action
|
|
336
|
+
* the user-facing workspaces toggle: applies to this session and
|
|
337
|
+
* becomes their default for sessions that don't specify one. Persisted
|
|
338
|
+
* only here, on an explicit toggle — an autorun mirroring the resolved
|
|
339
|
+
* value would bake the admin default into every visitor's localStorage
|
|
340
|
+
* on first load, so a later admin change could never reach them.
|
|
341
|
+
*/
|
|
342
|
+
setUseWorkspacesPreference(useWorkspaces) {
|
|
343
|
+
self.useWorkspaces = useWorkspaces;
|
|
344
|
+
self.setPreferenceOverride('useWorkspaces', useWorkspaces);
|
|
345
|
+
},
|
|
346
|
+
/**
|
|
347
|
+
* #action
|
|
348
|
+
* drop both this session's explicit value and the user's override so
|
|
349
|
+
* workspaces falls back to the admin default
|
|
350
|
+
*/
|
|
351
|
+
resetUseWorkspaces() {
|
|
352
|
+
self.useWorkspaces = undefined;
|
|
353
|
+
self.clearPreferenceOverride('useWorkspaces');
|
|
354
|
+
},
|
|
355
|
+
afterAttach() {
|
|
356
|
+
addDisposer(self, autorun(function stickyViewHeadersAutorun() {
|
|
357
|
+
localStorageSetBoolean('stickyViewHeaders', self.stickyViewHeaders);
|
|
358
|
+
}, { name: 'StickyViewHeaders' }));
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
})
|
|
74
362
|
.postProcessSnapshot(snap => {
|
|
363
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
75
364
|
if (!snap) {
|
|
76
365
|
return snap;
|
|
77
366
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
367
|
+
// stickyViewHeaders is a personal per-browser UI preference, not shared
|
|
368
|
+
// view state: destructure it out so it never lands in the snapshot. It
|
|
369
|
+
// stays localStorage-backed, so each browser keeps its own value.
|
|
370
|
+
// useWorkspaces stays: it changes layout intent (and pairs with the
|
|
371
|
+
// `layout` tree the snapshot carries), which is meaningful to share.
|
|
372
|
+
const { stickyViewHeaders, ...rest } = snap;
|
|
373
|
+
return rest;
|
|
84
374
|
});
|
|
85
375
|
}
|
|
376
|
+
/** Type guard for SessionWithMultipleViews */
|
|
86
377
|
export function isSessionWithMultipleViews(session) {
|
|
87
378
|
return isBaseSession(session) && 'views' in session;
|
|
88
379
|
}
|