@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.
@@ -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.clear();
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 the user closed keeps
166
- // its references and its autoruns. One that pointed into the view is
167
- // now empty, so hide it rather than leave the panel showing what it no
168
- // longer has.
169
- for (const [, widget] of self.widgets) {
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
  }
@@ -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' || (!('adapter' in track) && 'uri' in track);
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 { getDescriptionHtmlUrl, rebaseDescriptionHtml, } from './descriptionHtml.js';
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
- const DescriptionCard = observer(function DescriptionCard({ url, }) {
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 (_jsxs(BaseCard, { title: "Description", children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", component: "div", className: classes.provenance, children: ["From the track hub, written for the UCSC Genome Browser \u2014 some display options it describes differ in JBrowse.", ' ', _jsx(Link, { href: url, target: "_blank", rel: "noopener noreferrer", children: "View original" })] }), error ? (_jsx(ErrorBanner, { error: error })) : isLoading || html === undefined ? (_jsx(LoadingEllipses, { message: "Loading description" })) : (_jsx(SanitizedHTML, { html: html }))] }));
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 url = getDescriptionHtmlUrl(readConfObject(config, 'metadata'));
23
- return url ? _jsx(DescriptionCard, { url: url }) : null;
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
- * The URL of a track's prose description page in its `metadata`, as a UCSC
3
- * track hub supplies it, and undefined for anything that is not an http(s) url
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 rather than a bare url, so the href comes back through a parse
12
- * rather than a regex.
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 getDescriptionHtmlUrl(metadata: unknown): string | undefined;
36
+ export declare function omitTrackDescription(metadata: Record<string, unknown>): Record<string, unknown>;
15
37
  /**
16
- * A fetched description page as markup for `SanitizedHTML`: links and images
17
- * resolved against the page they came from rather than the JBrowse origin, and
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 descriptionUrl(value) {
5
- const href = new DOMParser()
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
- * The URL of a track's prose description page in its `metadata`, as a UCSC
14
- * track hub supplies it, and undefined for anything that is not an http(s) url
15
- * — the value reaches `openLocation` and an anchor's href, so a `javascript:`
16
- * or `file://` one does not become one here.
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 rather than a bare url, so the href comes back through a parse
23
- * rather than a regex.
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 getDescriptionHtmlUrl(metadata) {
26
- return [metadata, isRecord(metadata) ? metadata.ucsc : undefined]
27
- .flatMap(record => (isRecord(record) ? [record.html, record.htmlPath] : []))
28
- .filter(candidate => typeof candidate === 'string')
29
- .map(candidate => descriptionUrl(candidate))
30
- .find(url => url !== undefined);
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 fetched description page as markup for `SanitizedHTML`: links and images
42
- * resolved against the page they came from rather than the JBrowse origin, and
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.10",
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.6.1",
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.10",
37
- "@jbrowse/render-core": "^5.0.0-beta.10"
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",