@jbrowse/product-core 5.0.0-beta.10 → 5.0.0-beta.11
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/Session/DrawerWidgets.d.ts +2 -0
- package/esm/Session/DrawerWidgets.js +12 -3
- package/esm/Session/MultipleViews.js +5 -5
- package/esm/controllerTracks.js +2 -2
- package/esm/ui/AboutDialogContents.js +2 -1
- package/esm/ui/DescriptionPanel.js +43 -6
- package/esm/ui/descriptionHtml.d.ts +31 -9
- package/esm/ui/descriptionHtml.js +74 -21
- package/package.json +4 -4
|
@@ -86,6 +86,8 @@ export declare function DrawerWidgetSessionMixin(pluginManager: PluginManager):
|
|
|
86
86
|
showWidget(widget: any): void;
|
|
87
87
|
/**
|
|
88
88
|
* #action
|
|
89
|
+
* closes the widget, and removes it from the session when its type sets
|
|
90
|
+
* `discardOnClose`. Minimizing the drawer is not closing.
|
|
89
91
|
*/
|
|
90
92
|
hideWidget(widget: any): void;
|
|
91
93
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isConfigurationModel } from '@jbrowse/core/configuration';
|
|
2
|
-
import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util';
|
|
3
|
-
import { addDisposer, getEnv, isAlive, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
import { localStorageGetItem, localStorageSetItem, scheduleDetachedDestroy, } from '@jbrowse/core/util';
|
|
3
|
+
import { addDisposer, detach, getEnv, isAlive, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
|
|
4
4
|
import { autorun } from 'mobx';
|
|
5
5
|
import { isBaseSession } from './BaseSession.js';
|
|
6
6
|
const minDrawerWidth = 128;
|
|
@@ -171,12 +171,19 @@ export function DrawerWidgetSessionMixin(pluginManager) {
|
|
|
171
171
|
},
|
|
172
172
|
/**
|
|
173
173
|
* #action
|
|
174
|
+
* closes the widget, and removes it from the session when its type sets
|
|
175
|
+
* `discardOnClose`. Minimizing the drawer is not closing.
|
|
174
176
|
*/
|
|
175
177
|
hideWidget(widget) {
|
|
176
178
|
self.activeWidgets.delete(widget.id);
|
|
177
179
|
if (self.activeWidgets.size === 0) {
|
|
178
180
|
self.poppedOut = false;
|
|
179
181
|
}
|
|
182
|
+
if (self.widgets.get(widget.id) === widget &&
|
|
183
|
+
pluginManager.getWidgetType(widget.type).discardOnClose) {
|
|
184
|
+
detach(widget);
|
|
185
|
+
scheduleDetachedDestroy(widget);
|
|
186
|
+
}
|
|
180
187
|
},
|
|
181
188
|
/**
|
|
182
189
|
* #action
|
|
@@ -213,7 +220,9 @@ export function DrawerWidgetSessionMixin(pluginManager) {
|
|
|
213
220
|
* #action
|
|
214
221
|
*/
|
|
215
222
|
hideAllWidgets() {
|
|
216
|
-
self.activeWidgets.
|
|
223
|
+
for (const widget of [...self.activeWidgets.values()]) {
|
|
224
|
+
this.hideWidget(widget);
|
|
225
|
+
}
|
|
217
226
|
self.poppedOut = false;
|
|
218
227
|
},
|
|
219
228
|
afterAttach() {
|
|
@@ -162,11 +162,11 @@ export function MultipleViewsSessionMixin(pluginManager) {
|
|
|
162
162
|
viewIdsWithin(view).includes(focusedViewId)) {
|
|
163
163
|
self.setFocusedViewId(undefined);
|
|
164
164
|
}
|
|
165
|
-
// Every widget, not the active ones: a widget
|
|
166
|
-
// its references and its autoruns. One that pointed into
|
|
167
|
-
// now empty, so
|
|
168
|
-
// longer has.
|
|
169
|
-
for (const
|
|
165
|
+
// Every widget, not the active ones: a closed widget that stays in the
|
|
166
|
+
// session keeps its references and its autoruns. One that pointed into
|
|
167
|
+
// the view is now empty, so close it rather than leave the panel
|
|
168
|
+
// showing what it no longer has.
|
|
169
|
+
for (const widget of [...self.widgets.values()]) {
|
|
170
170
|
if (dropRefsInto(widget, view)) {
|
|
171
171
|
self.hideWidget(widget);
|
|
172
172
|
}
|
package/esm/controllerTracks.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getEnv } from '@jbrowse/core/util';
|
|
2
2
|
import { withPageBaseUri } from '@jbrowse/core/util/addRelativeUris';
|
|
3
|
-
import { guessTrackConf } from '@jbrowse/core/util/tracks';
|
|
3
|
+
import { guessTrackConf, isLooseTrackConfig } from '@jbrowse/core/util/tracks';
|
|
4
4
|
import { isAlive } from '@jbrowse/mobx-state-tree';
|
|
5
5
|
import { resolveLocalFileUris } from './localFiles.js';
|
|
6
6
|
export function isLooseTrack(track) {
|
|
7
|
-
return typeof track === 'string' || (
|
|
7
|
+
return typeof track === 'string' || isLooseTrackConfig(track);
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Stamp the view's assembly onto a full-config track that omits
|
|
@@ -12,6 +12,7 @@ import DescriptionPanel from './DescriptionPanel.js';
|
|
|
12
12
|
import FileInfoPanel from './FileInfoPanel.js';
|
|
13
13
|
import HeaderButtons from './HeaderButtons.js';
|
|
14
14
|
import RefNameInfoDialog from './RefNameInfoDialog.js';
|
|
15
|
+
import { omitTrackDescription } from './descriptionHtml.js';
|
|
15
16
|
import { getAboutDialogConfig } from './util.js';
|
|
16
17
|
const useStyles = makeStyles()({
|
|
17
18
|
content: {
|
|
@@ -24,7 +25,7 @@ const AboutDialogContents = observer(function AboutDialogContents({ config, sess
|
|
|
24
25
|
const [showRefNames, setShowRefNames] = useState(false);
|
|
25
26
|
const { pluginManager } = getEnv(session);
|
|
26
27
|
const { config: shown, hideUris } = getAboutDialogConfig({ config, session });
|
|
27
|
-
return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: readConfObject(config), hideUris: hideUris, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: shown.metadata, hideUris: hideUris }) })) : null, _jsx(DescriptionPanel, { config: config, session: session }), _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
|
|
28
|
+
return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: readConfObject(config), hideUris: hideUris, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: omitTrackDescription(shown.metadata), hideUris: hideUris }) })) : null, _jsx(DescriptionPanel, { config: config, session: session }), _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
|
|
28
29
|
/** #extensionPoint Core-extraAboutPanel | sync | Add extra panels to a track's About dialog */
|
|
29
30
|
name: "Core-extraAboutPanel", props: { session, config } }), hideUris ? null : _jsx(FileInfoPanel, { config: config, session: session }), showRefNames ? (_jsx(RefNameInfoDialog, { session: session, config: config, onClose: () => {
|
|
30
31
|
setShowRefNames(false);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
|
|
3
3
|
import { readConfObject } from '@jbrowse/core/configuration';
|
|
4
4
|
import { ErrorBanner, LoadingEllipses, SanitizedHTML } from '@jbrowse/core/ui';
|
|
@@ -7,19 +7,56 @@ import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
|
7
7
|
import { useFetch } from '@jbrowse/core/util/useFetch';
|
|
8
8
|
import { Link, Typography } from '@mui/material';
|
|
9
9
|
import { observer } from 'mobx-react';
|
|
10
|
-
import {
|
|
10
|
+
import { getTrackDescription, rebaseDescriptionHtml, } from './descriptionHtml.js';
|
|
11
11
|
const useStyles = makeStyles()(theme => ({
|
|
12
12
|
provenance: {
|
|
13
13
|
marginBottom: theme.spacing(1),
|
|
14
14
|
},
|
|
15
|
+
prose: {
|
|
16
|
+
display: 'block',
|
|
17
|
+
'& h1, & h2': {
|
|
18
|
+
fontSize: theme.typography.pxToRem(16),
|
|
19
|
+
fontWeight: theme.typography.fontWeightBold,
|
|
20
|
+
margin: theme.spacing(2, 0, 0.5),
|
|
21
|
+
},
|
|
22
|
+
'& h3, & h4': {
|
|
23
|
+
fontSize: theme.typography.pxToRem(14),
|
|
24
|
+
fontWeight: theme.typography.fontWeightBold,
|
|
25
|
+
margin: theme.spacing(1.5, 0, 0.5),
|
|
26
|
+
},
|
|
27
|
+
'& > :first-child': {
|
|
28
|
+
marginTop: 0,
|
|
29
|
+
},
|
|
30
|
+
'& p': {
|
|
31
|
+
margin: theme.spacing(0, 0, 1),
|
|
32
|
+
},
|
|
33
|
+
'& table': {
|
|
34
|
+
borderCollapse: 'collapse',
|
|
35
|
+
margin: theme.spacing(1, 0),
|
|
36
|
+
},
|
|
37
|
+
'& th, & td': {
|
|
38
|
+
padding: theme.spacing(0.25, 1),
|
|
39
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
40
|
+
},
|
|
41
|
+
'& img': {
|
|
42
|
+
maxWidth: '100%',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
15
45
|
}));
|
|
16
|
-
|
|
46
|
+
function Prose({ html }) {
|
|
47
|
+
const { classes } = useStyles();
|
|
48
|
+
return _jsx(SanitizedHTML, { html: html, className: classes.prose });
|
|
49
|
+
}
|
|
50
|
+
function DescriptionCard({ url, children, }) {
|
|
17
51
|
const { classes } = useStyles();
|
|
52
|
+
return (_jsxs(BaseCard, { title: "Description", children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", component: "div", className: classes.provenance, children: ["The track's description, written for the UCSC Genome Browser \u2014 some display options it describes differ in JBrowse.", url ? (_jsxs(_Fragment, { children: [' ', _jsx(Link, { href: url, target: "_blank", rel: "noopener noreferrer", children: "View original" })] })) : null] }), children] }));
|
|
53
|
+
}
|
|
54
|
+
const FetchedDescription = observer(function FetchedDescription({ url, }) {
|
|
18
55
|
const { data: html, error, isLoading, } = useFetch(['trackDescriptionHtml', url], async (_name, url) => rebaseDescriptionHtml(await openLocation({ uri: url, locationType: 'UriLocation' }).readFile('utf8'), url));
|
|
19
|
-
return (
|
|
56
|
+
return (_jsx(DescriptionCard, { url: url, children: error ? (_jsx(ErrorBanner, { error: error })) : isLoading || html === undefined ? (_jsx(LoadingEllipses, { message: "Loading description" })) : (_jsx(Prose, { html: html })) }));
|
|
20
57
|
});
|
|
21
58
|
const DescriptionPanel = observer(function DescriptionPanel({ config, }) {
|
|
22
|
-
const
|
|
23
|
-
return url ? _jsx(
|
|
59
|
+
const description = getTrackDescription(readConfObject(config, 'metadata'));
|
|
60
|
+
return !description ? null : 'url' in description ? (_jsx(FetchedDescription, { url: description.url })) : (_jsx(DescriptionCard, { children: _jsx(Prose, { html: description.html }) }));
|
|
24
61
|
});
|
|
25
62
|
export default DescriptionPanel;
|
|
@@ -1,20 +1,42 @@
|
|
|
1
|
+
export type TrackDescription = {
|
|
2
|
+
url: string;
|
|
3
|
+
} | {
|
|
4
|
+
html: string;
|
|
5
|
+
};
|
|
1
6
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* — the value reaches `openLocation` and an anchor's href, so a `javascript:`
|
|
5
|
-
* or `file://` one does not become one here.
|
|
7
|
+
* A track's prose description in its `metadata`, as UCSC trackDb supplies it:
|
|
8
|
+
* a link to a page to fetch, or the page itself.
|
|
6
9
|
*
|
|
7
10
|
* Four spellings: `UCSCTrackHubConnection` spreads the trackDb stanza across
|
|
8
11
|
* `metadata`, the bulk converter behind genomes.jbrowse.org nests it under
|
|
9
12
|
* `metadata.ucsc` (~50,700 configs at permanent URLs), and the stanza key is
|
|
10
13
|
* `html` for a track and `htmlPath` for an assembly. Both producers write an
|
|
11
|
-
* escaped anchor
|
|
12
|
-
*
|
|
14
|
+
* escaped anchor for a hub's page, so the href comes back through a parse. The
|
|
15
|
+
* converter writes golden-path assemblies' pages inline, and only under `ucsc`
|
|
16
|
+
* is markup read as a page, so a track's own `html` column stays a column.
|
|
17
|
+
*
|
|
18
|
+
* A url is http(s) only: it reaches `openLocation` and an anchor's href.
|
|
19
|
+
* `path` names the key read, which the metadata card leaves out.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findTrackDescription(metadata: unknown): {
|
|
22
|
+
description: {
|
|
23
|
+
url: string;
|
|
24
|
+
html?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
url?: undefined;
|
|
27
|
+
html: string;
|
|
28
|
+
};
|
|
29
|
+
path: string[];
|
|
30
|
+
} | undefined;
|
|
31
|
+
export declare function getTrackDescription(metadata: unknown): TrackDescription | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* `metadata` without the key its description came from, since the Description
|
|
34
|
+
* card shows that one.
|
|
13
35
|
*/
|
|
14
|
-
export declare function
|
|
36
|
+
export declare function omitTrackDescription(metadata: Record<string, unknown>): Record<string, unknown>;
|
|
15
37
|
/**
|
|
16
|
-
* A
|
|
17
|
-
*
|
|
38
|
+
* A description page as markup for `SanitizedHTML`: links and images resolved
|
|
39
|
+
* against the page they came from rather than the JBrowse origin, and
|
|
18
40
|
* `<form>` dropped, which the sanitizer keeps and which in a document off a
|
|
19
41
|
* third-party origin is a credential prompt wearing the app's own chrome.
|
|
20
42
|
*
|
|
@@ -1,33 +1,86 @@
|
|
|
1
1
|
function isRecord(value) {
|
|
2
2
|
return typeof value === 'object' && value !== null;
|
|
3
3
|
}
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
.parseFromString(value, 'text/html')
|
|
7
|
-
.querySelector('a[href]')
|
|
8
|
-
?.getAttribute('href');
|
|
9
|
-
const candidate = typeof href === 'string' ? href : value.trim();
|
|
10
|
-
return /^https?:\/\//i.test(candidate) ? candidate : undefined;
|
|
4
|
+
function isHttpUrl(value) {
|
|
5
|
+
return /^https?:\/\//i.test(value);
|
|
11
6
|
}
|
|
12
7
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
8
|
+
* Where UCSC trackDb text sits when the golden-path assemblies the UCSC
|
|
9
|
+
* Genome Browser serves itself carry it inline: their `html` holds the page,
|
|
10
|
+
* written to render inside `hgTrackUi`, so its relative links (`hgTables`,
|
|
11
|
+
* `/goldenPath/help/…`) resolve against that.
|
|
12
|
+
*/
|
|
13
|
+
const UCSC_TRACK_PAGE = 'https://genome.ucsc.edu/cgi-bin/hgTrackUi';
|
|
14
|
+
function parseDescription(value, inline) {
|
|
15
|
+
const body = new DOMParser().parseFromString(value, 'text/html').body;
|
|
16
|
+
const [first] = body.children;
|
|
17
|
+
if (!first) {
|
|
18
|
+
const text = value.trim();
|
|
19
|
+
return isHttpUrl(text) ? { url: text } : undefined;
|
|
20
|
+
}
|
|
21
|
+
const loneAnchor = body.children.length === 1 &&
|
|
22
|
+
first.matches('a[href]') &&
|
|
23
|
+
body.textContent.trim() === first.textContent.trim();
|
|
24
|
+
if (loneAnchor) {
|
|
25
|
+
const href = first.getAttribute('href') ?? '';
|
|
26
|
+
return isHttpUrl(href) ? { url: href } : undefined;
|
|
27
|
+
}
|
|
28
|
+
return inline
|
|
29
|
+
? { html: rebaseDescriptionHtml(value, UCSC_TRACK_PAGE) }
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A track's prose description in its `metadata`, as UCSC trackDb supplies it:
|
|
34
|
+
* a link to a page to fetch, or the page itself.
|
|
17
35
|
*
|
|
18
36
|
* Four spellings: `UCSCTrackHubConnection` spreads the trackDb stanza across
|
|
19
37
|
* `metadata`, the bulk converter behind genomes.jbrowse.org nests it under
|
|
20
38
|
* `metadata.ucsc` (~50,700 configs at permanent URLs), and the stanza key is
|
|
21
39
|
* `html` for a track and `htmlPath` for an assembly. Both producers write an
|
|
22
|
-
* escaped anchor
|
|
23
|
-
*
|
|
40
|
+
* escaped anchor for a hub's page, so the href comes back through a parse. The
|
|
41
|
+
* converter writes golden-path assemblies' pages inline, and only under `ucsc`
|
|
42
|
+
* is markup read as a page, so a track's own `html` column stays a column.
|
|
43
|
+
*
|
|
44
|
+
* A url is http(s) only: it reaches `openLocation` and an anchor's href.
|
|
45
|
+
* `path` names the key read, which the metadata card leaves out.
|
|
24
46
|
*/
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
export function findTrackDescription(metadata) {
|
|
48
|
+
const ucsc = isRecord(metadata) ? metadata.ucsc : undefined;
|
|
49
|
+
const places = [
|
|
50
|
+
{ record: metadata, prefix: [], inline: false },
|
|
51
|
+
{ record: ucsc, prefix: ['ucsc'], inline: true },
|
|
52
|
+
];
|
|
53
|
+
for (const { record, prefix, inline } of places) {
|
|
54
|
+
for (const key of ['html', 'htmlPath']) {
|
|
55
|
+
const value = isRecord(record) ? record[key] : undefined;
|
|
56
|
+
const description = typeof value === 'string' ? parseDescription(value, inline) : undefined;
|
|
57
|
+
if (description) {
|
|
58
|
+
return { description, path: [...prefix, key] };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
export function getTrackDescription(metadata) {
|
|
65
|
+
return findTrackDescription(metadata)?.description;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `metadata` without the key its description came from, since the Description
|
|
69
|
+
* card shows that one.
|
|
70
|
+
*/
|
|
71
|
+
export function omitTrackDescription(metadata) {
|
|
72
|
+
const path = findTrackDescription(metadata)?.path;
|
|
73
|
+
if (!path) {
|
|
74
|
+
return metadata;
|
|
75
|
+
}
|
|
76
|
+
const [key = '', nestedKey] = path;
|
|
77
|
+
const nested = metadata[key];
|
|
78
|
+
return nestedKey !== undefined && isRecord(nested)
|
|
79
|
+
? { ...metadata, [key]: without(nested, nestedKey) }
|
|
80
|
+
: without(metadata, key);
|
|
81
|
+
}
|
|
82
|
+
function without(record, key) {
|
|
83
|
+
return Object.fromEntries(Object.entries(record).filter(([k]) => k !== key));
|
|
31
84
|
}
|
|
32
85
|
function absolute(value, baseUrl) {
|
|
33
86
|
try {
|
|
@@ -38,8 +91,8 @@ function absolute(value, baseUrl) {
|
|
|
38
91
|
}
|
|
39
92
|
}
|
|
40
93
|
/**
|
|
41
|
-
* A
|
|
42
|
-
*
|
|
94
|
+
* A description page as markup for `SanitizedHTML`: links and images resolved
|
|
95
|
+
* against the page they came from rather than the JBrowse origin, and
|
|
43
96
|
* `<form>` dropped, which the sanitizer keeps and which in a document off a
|
|
44
97
|
* third-party origin is a credential prompt wearing the app's own chrome.
|
|
45
98
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/product-core",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "JBrowse 2 code shared between products but not used by plugins",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"esm"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@jbrowse/mobx-state-tree": "^6.
|
|
31
|
+
"@jbrowse/mobx-state-tree": "^6.7.0",
|
|
32
32
|
"@mui/icons-material": "^9.4.0",
|
|
33
33
|
"@mui/material": "^9.4.0",
|
|
34
34
|
"mobx": "^7.0.5",
|
|
35
35
|
"mobx-react": "^10.0.2",
|
|
36
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
37
|
-
"@jbrowse/
|
|
36
|
+
"@jbrowse/render-core": "^5.0.0-beta.11",
|
|
37
|
+
"@jbrowse/core": "^5.0.0-beta.11"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"react": ">=19.0.0",
|