@jbrowse/plugin-data-management 2.6.2 → 2.6.3

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.
Files changed (61) hide show
  1. package/dist/HierarchicalTrackSelectorWidget/components/faceted/FacetedSelector.js +4 -5
  2. package/dist/HierarchicalTrackSelectorWidget/components/tree/HamburgerMenu.js +35 -19
  3. package/dist/HierarchicalTrackSelectorWidget/components/tree/HierarchicalHeader.js +10 -6
  4. package/dist/HierarchicalTrackSelectorWidget/components/tree/HierarchicalTree.d.ts +2 -1
  5. package/dist/HierarchicalTrackSelectorWidget/components/tree/TrackCategory.js +22 -0
  6. package/dist/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.js +1 -1
  7. package/dist/HierarchicalTrackSelectorWidget/components/util.d.ts +2 -1
  8. package/dist/HierarchicalTrackSelectorWidget/filterTracks.d.ts +12 -0
  9. package/dist/HierarchicalTrackSelectorWidget/filterTracks.js +32 -0
  10. package/dist/HierarchicalTrackSelectorWidget/generateHierarchy.d.ts +21 -0
  11. package/dist/HierarchicalTrackSelectorWidget/generateHierarchy.js +98 -0
  12. package/dist/HierarchicalTrackSelectorWidget/model.d.ts +68 -28
  13. package/dist/HierarchicalTrackSelectorWidget/model.js +149 -117
  14. package/dist/HierarchicalTrackSelectorWidget/util.d.ts +12 -0
  15. package/dist/HierarchicalTrackSelectorWidget/util.js +53 -0
  16. package/dist/PluginStoreWidget/components/AddCustomPluginDialog.d.ts +7 -0
  17. package/dist/PluginStoreWidget/components/{CustomPluginForm.js → AddCustomPluginDialog.js} +12 -21
  18. package/dist/PluginStoreWidget/components/DeletePluginDialog.d.ts +5 -0
  19. package/dist/PluginStoreWidget/components/DeletePluginDialog.js +28 -0
  20. package/dist/PluginStoreWidget/components/InstalledPlugin.d.ts +2 -5
  21. package/dist/PluginStoreWidget/components/InstalledPlugin.js +19 -50
  22. package/dist/PluginStoreWidget/components/InstalledPluginsList.d.ts +2 -3
  23. package/dist/PluginStoreWidget/components/InstalledPluginsList.js +6 -9
  24. package/dist/PluginStoreWidget/components/PluginCard.js +3 -5
  25. package/dist/PluginStoreWidget/components/PluginStoreWidget.js +11 -39
  26. package/dist/PluginStoreWidget/components/util.d.ts +5 -0
  27. package/dist/PluginStoreWidget/components/util.js +29 -0
  28. package/dist/PluginStoreWidget/model.d.ts +2 -2
  29. package/dist/PluginStoreWidget/model.js +2 -2
  30. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetedSelector.js +1 -2
  31. package/esm/HierarchicalTrackSelectorWidget/components/tree/HamburgerMenu.js +34 -18
  32. package/esm/HierarchicalTrackSelectorWidget/components/tree/HierarchicalHeader.js +10 -6
  33. package/esm/HierarchicalTrackSelectorWidget/components/tree/HierarchicalTree.d.ts +2 -1
  34. package/esm/HierarchicalTrackSelectorWidget/components/tree/TrackCategory.js +22 -0
  35. package/esm/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.js +1 -1
  36. package/esm/HierarchicalTrackSelectorWidget/components/util.d.ts +2 -1
  37. package/esm/HierarchicalTrackSelectorWidget/filterTracks.d.ts +12 -0
  38. package/esm/HierarchicalTrackSelectorWidget/filterTracks.js +28 -0
  39. package/esm/HierarchicalTrackSelectorWidget/generateHierarchy.d.ts +21 -0
  40. package/esm/HierarchicalTrackSelectorWidget/generateHierarchy.js +94 -0
  41. package/esm/HierarchicalTrackSelectorWidget/model.d.ts +68 -28
  42. package/esm/HierarchicalTrackSelectorWidget/model.js +151 -116
  43. package/esm/HierarchicalTrackSelectorWidget/util.d.ts +12 -0
  44. package/esm/HierarchicalTrackSelectorWidget/util.js +45 -0
  45. package/esm/PluginStoreWidget/components/AddCustomPluginDialog.d.ts +7 -0
  46. package/esm/PluginStoreWidget/components/{CustomPluginForm.js → AddCustomPluginDialog.js} +12 -21
  47. package/esm/PluginStoreWidget/components/DeletePluginDialog.d.ts +5 -0
  48. package/esm/PluginStoreWidget/components/DeletePluginDialog.js +22 -0
  49. package/esm/PluginStoreWidget/components/InstalledPlugin.d.ts +2 -5
  50. package/esm/PluginStoreWidget/components/InstalledPlugin.js +22 -53
  51. package/esm/PluginStoreWidget/components/InstalledPluginsList.d.ts +2 -3
  52. package/esm/PluginStoreWidget/components/InstalledPluginsList.js +6 -9
  53. package/esm/PluginStoreWidget/components/PluginCard.js +3 -5
  54. package/esm/PluginStoreWidget/components/PluginStoreWidget.js +12 -40
  55. package/esm/PluginStoreWidget/components/util.d.ts +5 -0
  56. package/esm/PluginStoreWidget/components/util.js +25 -0
  57. package/esm/PluginStoreWidget/model.d.ts +2 -2
  58. package/esm/PluginStoreWidget/model.js +1 -1
  59. package/package.json +2 -2
  60. package/dist/PluginStoreWidget/components/CustomPluginForm.d.ts +0 -9
  61. package/esm/PluginStoreWidget/components/CustomPluginForm.d.ts +0 -9
@@ -7,14 +7,11 @@ const react_1 = __importDefault(require("react"));
7
7
  const mobx_react_1 = require("mobx-react");
8
8
  const material_1 = require("@mui/material");
9
9
  const InstalledPlugin_1 = __importDefault(require("./InstalledPlugin"));
10
- function InstalledPluginsList({ pluginManager, model, }) {
10
+ exports.default = (0, mobx_react_1.observer)(function InstalledPluginsList({ pluginManager, model, }) {
11
11
  const { plugins } = pluginManager;
12
- const corePlugins = new Set(plugins
13
- .filter(p => { var _a; return (_a = pluginManager.pluginMetadata[p.name]) === null || _a === void 0 ? void 0 : _a.isCore; })
14
- .map(p => p.name));
15
- const externalPlugins = plugins.filter(plugin => !corePlugins.has(plugin.name));
12
+ const { filterText } = model;
13
+ const externalPlugins = plugins.filter(p => { var _a; return !((_a = pluginManager.pluginMetadata[p.name]) === null || _a === void 0 ? void 0 : _a.isCore); });
16
14
  return (react_1.default.createElement(material_1.List, null, externalPlugins.length > 0 ? (externalPlugins
17
- .filter(plugin => plugin.name.toLowerCase().includes(model.filterText.toLowerCase()))
18
- .map(plugin => (react_1.default.createElement(InstalledPlugin_1.default, { key: plugin.name, plugin: plugin, model: model, pluginManager: pluginManager })))) : (react_1.default.createElement(material_1.Typography, null, "No plugins currently installed"))));
19
- }
20
- exports.default = (0, mobx_react_1.observer)(InstalledPluginsList);
15
+ .filter(p => p.name.toLowerCase().includes(filterText.toLowerCase()))
16
+ .map(p => react_1.default.createElement(InstalledPlugin_1.default, { key: p.name, plugin: p, model: model }))) : (react_1.default.createElement(material_1.Typography, null, "No plugins currently installed"))));
17
+ });
@@ -39,7 +39,7 @@ const Add_1 = __importDefault(require("@mui/icons-material/Add"));
39
39
  const Check_1 = __importDefault(require("@mui/icons-material/Check"));
40
40
  const useStyles = (0, mui_1.makeStyles)()({
41
41
  card: {
42
- margin: '1em',
42
+ margin: '0.5em',
43
43
  },
44
44
  icon: {
45
45
  marginLeft: '0.5em',
@@ -51,7 +51,6 @@ const useStyles = (0, mui_1.makeStyles)()({
51
51
  dataField: {
52
52
  display: 'flex',
53
53
  alignItems: 'center',
54
- margin: '0.4em 0em',
55
54
  },
56
55
  });
57
56
  exports.default = (0, mobx_react_1.observer)(function PluginCard({ plugin, model, adminMode, }) {
@@ -67,9 +66,8 @@ exports.default = (0, mobx_react_1.observer)(function PluginCard({ plugin, model
67
66
  const { jbrowse } = rootModel;
68
67
  return (react_1.default.createElement(material_1.Card, { variant: "outlined", key: plugin.name, className: classes.card },
69
68
  react_1.default.createElement(material_1.CardContent, null,
70
- react_1.default.createElement("div", { className: classes.dataField },
71
- react_1.default.createElement(material_1.Typography, { variant: "h5" },
72
- react_1.default.createElement(material_1.Link, { href: `${plugin.location}#readme`, target: "_blank", rel: "noopener" }, plugin.name))),
69
+ react_1.default.createElement(material_1.Typography, { variant: "h5" },
70
+ react_1.default.createElement(material_1.Link, { href: `${plugin.location}#readme`, target: "_blank", rel: "noopener" }, plugin.name)),
73
71
  react_1.default.createElement("div", { className: classes.dataField },
74
72
  react_1.default.createElement(Person_1.default, { style: { marginRight: '0.5em' } }),
75
73
  react_1.default.createElement(material_1.Typography, null, plugin.authors.join(', '))),
@@ -40,16 +40,14 @@ const InfoOutlined_1 = __importDefault(require("@mui/icons-material/InfoOutlined
40
40
  // locals
41
41
  const InstalledPluginsList_1 = __importDefault(require("./InstalledPluginsList"));
42
42
  const PluginCard_1 = __importDefault(require("./PluginCard"));
43
- const CustomPluginForm_1 = __importDefault(require("./CustomPluginForm"));
43
+ const util_2 = require("./util");
44
+ // lazies
45
+ const AddCustomPluginDialog = (0, react_1.lazy)(() => Promise.resolve().then(() => __importStar(require('./AddCustomPluginDialog'))));
44
46
  const useStyles = (0, mui_1.makeStyles)()(theme => ({
45
- root: {
46
- margin: theme.spacing(1),
47
- },
48
47
  expandIcon: {
49
48
  color: theme.palette.tertiary.contrastText,
50
49
  },
51
50
  adminBadge: {
52
- margin: '0.5em',
53
51
  borderRadius: 3,
54
52
  backgroundColor: theme.palette.quaternary.main,
55
53
  padding: '1em',
@@ -57,43 +55,17 @@ const useStyles = (0, mui_1.makeStyles)()(theme => ({
57
55
  alignContent: 'center',
58
56
  },
59
57
  customPluginButton: {
60
- margin: '0.5em',
58
+ margin: '1em auto',
61
59
  display: 'flex',
62
- justifyContent: 'center',
63
60
  },
64
61
  }));
65
62
  function PluginStoreWidget({ model }) {
66
63
  const { classes } = useStyles();
67
- const [pluginArray, setPluginArray] = (0, react_1.useState)();
68
- const [error, setError] = (0, react_1.useState)();
69
- const [customPluginFormOpen, setCustomPluginFormOpen] = (0, react_1.useState)(false);
64
+ const { plugins, error } = (0, util_2.useFetchPlugins)();
65
+ const [open, setOpen] = (0, react_1.useState)(false);
70
66
  const { adminMode } = (0, util_1.getSession)(model);
71
67
  const { pluginManager } = (0, mobx_state_tree_1.getEnv)(model);
72
- (0, react_1.useEffect)(() => {
73
- const controller = new AbortController();
74
- const { signal } = controller;
75
- (async () => {
76
- try {
77
- const response = await fetch('https://jbrowse.org/plugin-store/plugins.json', { signal });
78
- if (!response.ok) {
79
- const err = await response.text();
80
- throw new Error(`Failed to fetch plugin data: ${response.status} ${response.statusText} ${err}`);
81
- }
82
- const array = await response.json();
83
- if (!signal.aborted) {
84
- setPluginArray(array.plugins);
85
- }
86
- }
87
- catch (e) {
88
- console.error(e);
89
- setError(e);
90
- }
91
- })();
92
- return () => {
93
- controller.abort();
94
- };
95
- }, []);
96
- return (react_1.default.createElement("div", { className: classes.root },
68
+ return (react_1.default.createElement("div", null,
97
69
  adminMode && (react_1.default.createElement(react_1.default.Fragment, null,
98
70
  !util_1.isElectron && (react_1.default.createElement("div", { className: classes.adminBadge },
99
71
  react_1.default.createElement(InfoOutlined_1.default, { style: { marginRight: '0.3em' } }),
@@ -101,9 +73,9 @@ function PluginStoreWidget({ model }) {
101
73
  "You are using the ",
102
74
  react_1.default.createElement("code", null, "admin-server"),
103
75
  ". Any changes you make will be saved to your configuration file. You also have the ability to add custom plugins that are not in the store."))),
104
- react_1.default.createElement("div", { className: classes.customPluginButton },
105
- react_1.default.createElement(material_1.Button, { variant: "contained", onClick: () => setCustomPluginFormOpen(true) }, "Add custom plugin")),
106
- react_1.default.createElement(CustomPluginForm_1.default, { open: customPluginFormOpen, onClose: () => setCustomPluginFormOpen(false), model: model }))),
76
+ react_1.default.createElement(material_1.Button, { className: classes.customPluginButton, variant: "contained", onClick: () => setOpen(true) }, "Add custom plugin"),
77
+ open ? (react_1.default.createElement(react_1.Suspense, { fallback: react_1.default.createElement(react_1.default.Fragment, null) },
78
+ react_1.default.createElement(AddCustomPluginDialog, { onClose: () => setOpen(false), model: model }))) : null)),
107
79
  react_1.default.createElement(material_1.TextField, { label: "Filter plugins", value: model.filterText, onChange: event => model.setFilterText(event.target.value), fullWidth: true, InputProps: {
108
80
  endAdornment: (react_1.default.createElement(material_1.InputAdornment, { position: "end" },
109
81
  react_1.default.createElement(material_1.IconButton, { onClick: () => model.clearFilterText() },
@@ -117,7 +89,7 @@ function PluginStoreWidget({ model }) {
117
89
  react_1.default.createElement(material_1.Accordion, { defaultExpanded: true },
118
90
  react_1.default.createElement(material_1.AccordionSummary, { expandIcon: react_1.default.createElement(ExpandMore_1.default, { className: classes.expandIcon }) },
119
91
  react_1.default.createElement(material_1.Typography, { variant: "h5" }, "Available plugins")),
120
- error ? (react_1.default.createElement(material_1.Typography, { color: "error" }, `${error}`)) : pluginArray ? (pluginArray
92
+ error ? (react_1.default.createElement(material_1.Typography, { color: "error" }, `${error}`)) : plugins ? (plugins
121
93
  .filter(plugin => {
122
94
  // If plugin only has cjsUrl, don't display outside desktop
123
95
  return (!(util_1.isElectron && plugin.cjsUrl) &&
@@ -0,0 +1,5 @@
1
+ import { JBrowsePlugin } from '@jbrowse/core/util/types';
2
+ export declare function useFetchPlugins(): {
3
+ plugins: JBrowsePlugin[] | undefined;
4
+ error: unknown;
5
+ };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFetchPlugins = void 0;
4
+ const react_1 = require("react");
5
+ function useFetchPlugins() {
6
+ const [plugins, setPlugins] = (0, react_1.useState)();
7
+ const [error, setError] = (0, react_1.useState)();
8
+ (0, react_1.useEffect)(() => {
9
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
10
+ ;
11
+ (async () => {
12
+ try {
13
+ const res = await fetch('https://jbrowse.org/plugin-store/plugins.json');
14
+ if (!res.ok) {
15
+ const err = await res.text();
16
+ throw new Error(`HTTP ${res.status} fetching plugins: ${err}`);
17
+ }
18
+ const array = await res.json();
19
+ setPlugins(array.plugins);
20
+ }
21
+ catch (e) {
22
+ console.error(e);
23
+ setError(e);
24
+ }
25
+ })();
26
+ }, []);
27
+ return { plugins, error };
28
+ }
29
+ exports.useFetchPlugins = useFetchPlugins;
@@ -1,6 +1,6 @@
1
1
  import { Instance } from 'mobx-state-tree';
2
2
  import PluginManager from '@jbrowse/core/PluginManager';
3
- export default function f(pluginManager: PluginManager): import("mobx-state-tree").IModelType<{
3
+ export default function stateModelFactory(pluginManager: PluginManager): import("mobx-state-tree").IModelType<{
4
4
  id: import("mobx-state-tree").IOptionalIType<import("mobx-state-tree").ISimpleType<string>, [undefined]>;
5
5
  type: import("mobx-state-tree").ISimpleType<"PluginStoreWidget">;
6
6
  filterText: import("mobx-state-tree").IType<string | undefined, string, string>;
@@ -9,5 +9,5 @@ export default function f(pluginManager: PluginManager): import("mobx-state-tree
9
9
  clearFilterText(): void;
10
10
  setFilterText(newText: string): void;
11
11
  }, import("mobx-state-tree")._NotCustomized, import("mobx-state-tree")._NotCustomized>;
12
- export type PluginStoreStateModel = ReturnType<typeof f>;
12
+ export type PluginStoreStateModel = ReturnType<typeof stateModelFactory>;
13
13
  export type PluginStoreModel = Instance<PluginStoreStateModel>;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const mobx_state_tree_1 = require("mobx-state-tree");
4
4
  const mst_1 = require("@jbrowse/core/util/types/mst");
5
- function f(pluginManager) {
5
+ function stateModelFactory(pluginManager) {
6
6
  return mobx_state_tree_1.types
7
7
  .model('PluginStoreModel', {
8
8
  id: mst_1.ElementId,
@@ -19,4 +19,4 @@ function f(pluginManager) {
19
19
  },
20
20
  }));
21
21
  }
22
- exports.default = f;
22
+ exports.default = stateModelFactory;
@@ -14,8 +14,7 @@ import { getEnv, getSession, measureGridWidth, useDebounce, } from '@jbrowse/cor
14
14
  import { readConfObject, } from '@jbrowse/core/configuration';
15
15
  // icons
16
16
  import MoreHoriz from '@mui/icons-material/MoreHoriz';
17
- // locals
18
- import { matches } from '../../model';
17
+ import { matches } from '../../util';
19
18
  import FacetedHeader from './FacetedHeader';
20
19
  import FacetFilters from './FacetFilters';
21
20
  import { getRootKeys } from './util';
@@ -1,10 +1,8 @@
1
1
  import React, { Suspense, lazy, useState } from 'react';
2
- import { IconButton } from '@mui/material';
3
- import { makeStyles } from 'tss-react/mui';
4
2
  import { observer } from 'mobx-react';
5
- import JBrowseMenu from '@jbrowse/core/ui/Menu';
6
3
  import { getSession, isSessionModelWithConnectionEditing, isSessionModelWithConnections, isSessionModelWithWidgets, isSessionWithAddTracks, } from '@jbrowse/core/util';
7
4
  import { readConfObject, } from '@jbrowse/core/configuration';
5
+ import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
8
6
  // icons
9
7
  import MenuIcon from '@mui/icons-material/Menu';
10
8
  // lazy components
@@ -12,20 +10,12 @@ const CloseConnectionDlg = lazy(() => import('../dialogs/CloseConnectionDialog')
12
10
  const DeleteConnectionDlg = lazy(() => import('../dialogs/DeleteConnectionDialog'));
13
11
  const ManageConnectionsDlg = lazy(() => import('../dialogs/ManageConnectionsDialog'));
14
12
  const ToggleConnectionsDlg = lazy(() => import('../dialogs/ToggleConnectionsDialog'));
15
- const useStyles = makeStyles()(theme => ({
16
- menuIcon: {
17
- marginRight: theme.spacing(1),
18
- marginBottom: 0,
19
- },
20
- }));
21
13
  export default observer(function HamburgerMenu({ model, }) {
22
14
  const session = getSession(model);
23
- const [menuEl, setMenuEl] = useState();
24
15
  const [modalInfo, setModalInfo] = useState();
25
16
  const [deleteDlgDetails, setDeleteDlgDetails] = useState();
26
17
  const [connectionToggleOpen, setConnectionToggleOpen] = useState(false);
27
18
  const [connectionManagerOpen, setConnectionManagerOpen] = useState(false);
28
- const { classes } = useStyles();
29
19
  function breakConnection(connectionConf, deletingConnection) {
30
20
  const name = readConfObject(connectionConf, 'name');
31
21
  const result = session.prepareToBreakConnection(connectionConf);
@@ -48,12 +38,7 @@ export default observer(function HamburgerMenu({ model, }) {
48
38
  }
49
39
  }
50
40
  return (React.createElement(React.Fragment, null,
51
- React.createElement(IconButton, { className: classes.menuIcon, onClick: event => setMenuEl(event.currentTarget) },
52
- React.createElement(MenuIcon, null)),
53
- React.createElement(JBrowseMenu, { anchorEl: menuEl, open: Boolean(menuEl), onMenuItemClick: (_, callback) => {
54
- callback();
55
- setMenuEl(undefined);
56
- }, onClose: () => setMenuEl(undefined), menuItems: [
41
+ React.createElement(CascadingMenuButton, { menuItems: [
57
42
  ...(isSessionWithAddTracks(session)
58
43
  ? [
59
44
  {
@@ -92,7 +77,38 @@ export default observer(function HamburgerMenu({ model, }) {
92
77
  },
93
78
  ]
94
79
  : []),
95
- ] }),
80
+ { type: 'divider' },
81
+ {
82
+ label: 'Sort tracks by name',
83
+ type: 'checkbox',
84
+ checked: model.activeSortTrackNames,
85
+ onClick: () => model.setSortTrackNames(!model.activeSortTrackNames),
86
+ },
87
+ {
88
+ label: 'Sort categories by name',
89
+ type: 'checkbox',
90
+ checked: model.activeSortCategories,
91
+ onClick: () => model.setSortCategories(!model.activeSortCategories),
92
+ },
93
+ { type: 'divider' },
94
+ ...(model.hasAnySubcategories
95
+ ? [
96
+ {
97
+ label: 'Collapse subcategories',
98
+ onClick: () => model.collapseSubCategories(),
99
+ },
100
+ ]
101
+ : []),
102
+ {
103
+ label: 'Collapse top-level categories',
104
+ onClick: () => model.collapseTopLevelCategories(),
105
+ },
106
+ {
107
+ label: 'Expand all categories',
108
+ onClick: () => model.expandAllCategories(),
109
+ },
110
+ ] },
111
+ React.createElement(MenuIcon, null)),
96
112
  React.createElement(Suspense, { fallback: React.createElement(React.Fragment, null) },
97
113
  modalInfo ? (React.createElement(CloseConnectionDlg, { modalInfo: modalInfo, onClose: () => setModalInfo(undefined) })) : null,
98
114
  deleteDlgDetails ? (React.createElement(DeleteConnectionDlg, { handleClose: () => setDeleteDlgDetails(undefined), deleteDialogDetails: deleteDlgDetails, session: session })) : null,
@@ -17,19 +17,23 @@ const useStyles = makeStyles()(theme => ({
17
17
  marginBottom: 0,
18
18
  },
19
19
  }));
20
+ const SearchTracksTextField = observer(function ({ model, }) {
21
+ const { filterText } = model;
22
+ const { classes } = useStyles();
23
+ return (React.createElement(TextField, { className: classes.searchBox, label: "Filter tracks", value: filterText, onChange: event => model.setFilterText(event.target.value), fullWidth: true, InputProps: {
24
+ endAdornment: (React.createElement(InputAdornment, { position: "end" },
25
+ React.createElement(IconButton, { onClick: () => model.clearFilterText() },
26
+ React.createElement(ClearIcon, null)))),
27
+ } }));
28
+ });
20
29
  function HierarchicalTrackSelectorHeader({ model, setHeaderHeight, }) {
21
30
  const { classes } = useStyles();
22
31
  const [facetedOpen, setFacetedOpen] = useState(false);
23
- const { filterText } = model;
24
32
  return (React.createElement("div", { ref: ref => setHeaderHeight((ref === null || ref === void 0 ? void 0 : ref.getBoundingClientRect().height) || 0), "data-testid": "hierarchical_track_selector" },
25
33
  React.createElement("div", { style: { display: 'flex' } },
26
34
  React.createElement(HamburgerMenu, { model: model }),
27
35
  React.createElement(ShoppingCart, { model: model }),
28
- React.createElement(TextField, { className: classes.searchBox, label: "Filter tracks", value: filterText, onChange: event => model.setFilterText(event.target.value), fullWidth: true, InputProps: {
29
- endAdornment: (React.createElement(InputAdornment, { position: "end" },
30
- React.createElement(IconButton, { onClick: () => model.clearFilterText() },
31
- React.createElement(ClearIcon, null)))),
32
- } }),
36
+ React.createElement(SearchTracksTextField, { model: model }),
33
37
  React.createElement(Button, { className: classes.menuIcon, onClick: () => setFacetedOpen(true) }, "Open faceted selector")),
34
38
  React.createElement(Suspense, { fallback: React.createElement("div", null) }, facetedOpen ? (React.createElement(FacetedDialog, { handleClose: () => setFacetedOpen(false), model: model })) : null)));
35
39
  }
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { TreeNode, HierarchicalTrackSelectorModel } from '../../model';
2
+ import { TreeNode } from '../../generateHierarchy';
3
+ import { HierarchicalTrackSelectorModel } from '../../model';
3
4
  declare const _default: ({ height, tree, model, }: {
4
5
  height: number;
5
6
  tree: TreeNode;
@@ -52,6 +52,28 @@ export default function Category({ isOpen, setOpen, data, }) {
52
52
  model.removeFromSelection(getAllChildren(r));
53
53
  },
54
54
  },
55
+ {
56
+ label: 'Show all tracks',
57
+ onClick: () => {
58
+ var _a;
59
+ for (const entry of ((_a = treeToMap(tree).get(id)) === null || _a === void 0 ? void 0 : _a.children) || []) {
60
+ if (!entry.children.length) {
61
+ model.view.showTrack(entry.id);
62
+ }
63
+ }
64
+ },
65
+ },
66
+ {
67
+ label: 'Hide all tracks',
68
+ onClick: () => {
69
+ var _a;
70
+ for (const entry of ((_a = treeToMap(tree).get(id)) === null || _a === void 0 ? void 0 : _a.children) || []) {
71
+ if (!entry.children.length) {
72
+ model.view.hideTrack(entry.id);
73
+ }
74
+ }
75
+ },
76
+ },
55
77
  ], onMenuItemClick: (_event, callback) => {
56
78
  callback();
57
79
  setMenuEl(null);
@@ -31,7 +31,7 @@ export default function TrackLabel({ data }) {
31
31
  React.createElement(FormControlLabel, { className: classes.checkboxLabel, control: React.createElement(Checkbox, { className: classes.compactCheckbox, checked: checked, onChange: () => onChange(id), disabled: isUnsupported(name), inputProps: {
32
32
  // @ts-expect-error
33
33
  'data-testid': `htsTrackEntry-${id}`,
34
- } }), label: React.createElement("div", { style: { background: selected ? '#cccc' : undefined } },
34
+ } }), label: React.createElement("div", { "data-testid": `htsTrackLabel-${id}`, style: { background: selected ? '#cccc' : undefined } },
35
35
  React.createElement(SanitizedHTML, { html: name })) })),
36
36
  React.createElement(IconButton, { onClick: e => setInfo({ target: e.currentTarget, id, conf }), style: { padding: 0 }, "data-testid": `htsTrackEntryMenu-${id}` },
37
37
  React.createElement(MoreHorizIcon, null)),
@@ -1,5 +1,6 @@
1
1
  import { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
- import { HierarchicalTrackSelectorModel, TreeNode } from '../model';
2
+ import { HierarchicalTrackSelectorModel } from '../model';
3
+ import { TreeNode } from '../generateHierarchy';
3
4
  export interface NodeData {
4
5
  nestingLevel: number;
5
6
  checked: boolean;
@@ -0,0 +1,12 @@
1
+ import { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
+ export declare function filterTracks(tracks: AnyConfigurationModel[], self: {
3
+ view?: {
4
+ type: string;
5
+ trackSelectorAnyOverlap?: boolean;
6
+ };
7
+ assemblyNames: string[];
8
+ }): ({
9
+ [x: string]: any;
10
+ } & import("mobx-state-tree/dist/internal").NonEmptyObject & {
11
+ setSubschema(slotName: string, data: unknown): any;
12
+ } & import("mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>)[];
@@ -0,0 +1,28 @@
1
+ import { readConfObject, } from '@jbrowse/core/configuration';
2
+ import { getEnv, getSession, notEmpty } from '@jbrowse/core/util';
3
+ import { hasAllOverlap, hasAnyOverlap } from './util';
4
+ export function filterTracks(tracks, self) {
5
+ const { assemblyManager } = getSession(self);
6
+ const { pluginManager } = getEnv(self);
7
+ const { view } = self;
8
+ if (!view) {
9
+ return [];
10
+ }
11
+ const trackListAssemblies = self.assemblyNames
12
+ .map(a => assemblyManager.get(a))
13
+ .filter(notEmpty);
14
+ return tracks
15
+ .filter(c => {
16
+ const trackAssemblyNames = readConfObject(c, 'assemblyNames');
17
+ const trackAssemblies = trackAssemblyNames === null || trackAssemblyNames === void 0 ? void 0 : trackAssemblyNames.map(name => assemblyManager.get(name)).filter(notEmpty);
18
+ return view.trackSelectorAnyOverlap
19
+ ? hasAnyOverlap(trackAssemblies, trackListAssemblies)
20
+ : hasAllOverlap(trackAssemblies, trackListAssemblies);
21
+ })
22
+ .filter(c => {
23
+ const { displayTypes } = pluginManager.getViewType(view.type);
24
+ const compatDisplays = displayTypes.map(d => d.name);
25
+ const trackDisplays = c.displays.map((d) => d.type);
26
+ return hasAnyOverlap(compatDisplays, trackDisplays);
27
+ });
28
+ }
@@ -0,0 +1,21 @@
1
+ import { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
+ export type TreeNode = {
3
+ name: string;
4
+ id: string;
5
+ conf?: AnyConfigurationModel;
6
+ checked?: boolean;
7
+ isOpenByDefault?: boolean;
8
+ children: TreeNode[];
9
+ };
10
+ export declare function generateHierarchy(model: {
11
+ filterText: string;
12
+ activeSortTrackNames: boolean;
13
+ activeSortCategories: boolean;
14
+ view?: {
15
+ tracks: {
16
+ configuration: AnyConfigurationModel;
17
+ }[];
18
+ };
19
+ }, trackConfs: AnyConfigurationModel[], collapsed: {
20
+ get: (arg: string) => boolean | undefined;
21
+ }, extra?: string): TreeNode[];
@@ -0,0 +1,94 @@
1
+ import { readConfObject, } from '@jbrowse/core/configuration';
2
+ import { getSession } from '@jbrowse/core/util';
3
+ import { getTrackName } from '@jbrowse/core/util/tracks';
4
+ // locals
5
+ import { matches } from './util';
6
+ function sortConfs(confs, sortNames, sortCategories) {
7
+ // uses readConfObject instead of getTrackName so that the undefined
8
+ // reference sequence track is sorted to the top
9
+ const ret = confs.map(c => {
10
+ var _a, _b, _c;
11
+ return [
12
+ c,
13
+ readConfObject(c, 'name'),
14
+ ((_a = readConfObject(c, 'category')) === null || _a === void 0 ? void 0 : _a[0]) || '',
15
+ ((_b = readConfObject(c, 'category')) === null || _b === void 0 ? void 0 : _b[1]) || '',
16
+ ((_c = readConfObject(c, 'category')) === null || _c === void 0 ? void 0 : _c[2]) || '',
17
+ ];
18
+ });
19
+ if (sortNames) {
20
+ ret.sort((a, b) => a[1].localeCompare(b[1]));
21
+ }
22
+ if (sortCategories) {
23
+ // sort up to three sub-category levels, harder to code it to go deeper
24
+ // than this and likely rarely used
25
+ ret.sort((a, b) => {
26
+ if (a[2] !== b[2]) {
27
+ return a[2].localeCompare(b[2]);
28
+ }
29
+ else if (a[3] !== b[3]) {
30
+ return a[3].localeCompare(b[3]);
31
+ }
32
+ else if (a[4] !== b[4]) {
33
+ return a[4].localeCompare(b[4]);
34
+ }
35
+ return 0;
36
+ });
37
+ }
38
+ return ret.map(a => a[0]);
39
+ }
40
+ export function generateHierarchy(model, trackConfs, collapsed, extra) {
41
+ const hierarchy = { children: [] };
42
+ const { filterText, activeSortTrackNames, activeSortCategories, view } = model;
43
+ if (!view) {
44
+ return [];
45
+ }
46
+ const session = getSession(model);
47
+ const viewTracks = view.tracks;
48
+ const confs = trackConfs.filter(conf => matches(filterText, conf, session));
49
+ // uses getConf
50
+ for (const conf of sortConfs(confs, activeSortTrackNames, activeSortCategories)) {
51
+ // copy the categories since this array can be mutated downstream
52
+ const categories = [...(readConfObject(conf, 'category') || [])];
53
+ // hack where if trackId ends with sessionTrack, then push it to a
54
+ // category that starts with a space to force sort to the top
55
+ if (conf.trackId.endsWith('sessionTrack')) {
56
+ categories.unshift(' Session tracks');
57
+ }
58
+ let currLevel = hierarchy;
59
+ // find existing category to put track into or create it
60
+ for (let i = 0; i < categories.length; i++) {
61
+ const category = categories[i];
62
+ const ret = currLevel.children.find(c => c.name === category);
63
+ const id = [extra, categories.slice(0, i + 1).join(',')]
64
+ .filter(f => !!f)
65
+ .join('-');
66
+ if (!ret) {
67
+ const n = {
68
+ children: [],
69
+ name: category,
70
+ id,
71
+ isOpenByDefault: !collapsed.get(id),
72
+ };
73
+ currLevel.children.push(n);
74
+ currLevel = n;
75
+ }
76
+ else {
77
+ currLevel = ret;
78
+ }
79
+ }
80
+ // uses splice to try to put all leaf nodes above "category nodes" if you
81
+ // change the splice to a simple push and open
82
+ // test_data/test_order/config.json you will see the weirdness
83
+ const r = currLevel.children.findIndex(elt => elt.children.length);
84
+ const idx = r === -1 ? currLevel.children.length : r;
85
+ currLevel.children.splice(idx, 0, {
86
+ id: conf.trackId,
87
+ name: getTrackName(conf, session),
88
+ conf,
89
+ checked: viewTracks.some(f => f.configuration === conf),
90
+ children: [],
91
+ });
92
+ }
93
+ return hierarchy.children;
94
+ }