@jbrowse/plugin-data-management 4.1.13 → 4.1.15

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 (27) hide show
  1. package/esm/HierarchicalTrackSelectorWidget/components/ClearableSearchField.js +9 -8
  2. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetFilter.js +6 -10
  3. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetFilters.js +10 -17
  4. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetedDataGrid.d.ts +2 -1
  5. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetedDataGrid.js +18 -9
  6. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetedHeader.d.ts +3 -2
  7. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetedHeader.js +5 -3
  8. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetedSelector.d.ts +0 -6
  9. package/esm/HierarchicalTrackSelectorWidget/components/faceted/FacetedSelector.js +9 -13
  10. package/esm/HierarchicalTrackSelectorWidget/components/tree/HierarchicalHeader.js +9 -5
  11. package/esm/HierarchicalTrackSelectorWidget/components/tree/HierarchicalTree.js +34 -21
  12. package/esm/HierarchicalTrackSelectorWidget/components/tree/SharedTooltip.d.ts +5 -0
  13. package/esm/HierarchicalTrackSelectorWidget/components/tree/SharedTooltip.js +39 -0
  14. package/esm/HierarchicalTrackSelectorWidget/components/tree/TrackCategory.js +8 -3
  15. package/esm/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.d.ts +2 -8
  16. package/esm/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.js +12 -16
  17. package/esm/HierarchicalTrackSelectorWidget/components/tree/TreeItem.d.ts +2 -2
  18. package/esm/HierarchicalTrackSelectorWidget/facetedModel.d.ts +20 -4
  19. package/esm/HierarchicalTrackSelectorWidget/facetedModel.js +22 -11
  20. package/esm/HierarchicalTrackSelectorWidget/facetedUtil.d.ts +1 -1
  21. package/esm/HierarchicalTrackSelectorWidget/facetedUtil.js +17 -5
  22. package/esm/HierarchicalTrackSelectorWidget/generateHierarchy.js +1 -0
  23. package/esm/HierarchicalTrackSelectorWidget/model.d.ts +20 -4
  24. package/esm/HierarchicalTrackSelectorWidget/model.js +3 -2
  25. package/esm/HierarchicalTrackSelectorWidget/types.d.ts +1 -0
  26. package/esm/HierarchicalTrackSelectorWidget/util.js +4 -2
  27. package/package.json +11 -8
@@ -1,22 +1,23 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useState, useTransition } from 'react';
2
+ import { useEffect, useEffectEvent, useState } from 'react';
3
+ import { useDebounce } from '@jbrowse/core/util';
3
4
  import ClearIcon from '@mui/icons-material/Clear';
4
5
  import { IconButton, InputAdornment, TextField } from '@mui/material';
5
6
  export default function ClearableSearchField({ value, onChange, label, className, }) {
6
7
  const [localValue, setLocalValue] = useState(value);
7
- const [, startTransition] = useTransition();
8
+ const debouncedValue = useDebounce(localValue, 300);
9
+ const onChangeEvent = useEffectEvent(onChange);
8
10
  useEffect(() => {
9
11
  if (value === '') {
10
12
  setLocalValue('');
11
13
  }
12
14
  }, [value]);
15
+ useEffect(() => {
16
+ onChangeEvent(debouncedValue);
17
+ }, [debouncedValue]);
13
18
  return (_jsx(TextField, { className: className, label: label, value: localValue, onChange: event => {
14
- const newValue = event.target.value;
15
- setLocalValue(newValue);
16
- startTransition(() => {
17
- onChange(newValue);
18
- });
19
- }, fullWidth: true, slotProps: {
19
+ setLocalValue(event.target.value);
20
+ }, slotProps: {
20
21
  input: {
21
22
  endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: () => {
22
23
  setLocalValue('');
@@ -17,14 +17,10 @@ const useStyles = makeStyles()(theme => ({
17
17
  },
18
18
  }));
19
19
  function ClearButton({ onClick }) {
20
- return (_jsx(Tooltip, { title: "Clear selection on this facet filter", children: _jsx(IconButton, { onClick: () => {
21
- onClick();
22
- }, size: "small", children: _jsx(ClearIcon, {}) }) }));
20
+ return (_jsx(Tooltip, { title: "Clear selection on this facet filter", children: _jsx(IconButton, { onClick: onClick, size: "small", children: _jsx(ClearIcon, {}) }) }));
23
21
  }
24
22
  function ExpandButton({ visible, onClick, }) {
25
- return (_jsx(Tooltip, { title: "Minimize/expand this facet filter", children: _jsx(IconButton, { onClick: () => {
26
- onClick();
27
- }, size: "small", children: visible ? _jsx(MinimizeIcon, {}) : _jsx(AddIcon, {}) }) }));
23
+ return (_jsx(Tooltip, { title: "Minimize/expand this facet filter", children: _jsx(IconButton, { onClick: onClick, size: "small", children: visible ? _jsx(MinimizeIcon, {}) : _jsx(AddIcon, {}) }) }));
28
24
  }
29
25
  const FacetFilter = observer(function FacetFilter({ column, vals, model, }) {
30
26
  const { classes } = useStyles();
@@ -37,11 +33,11 @@ const FacetFilter = observer(function FacetFilter({ column, vals, model, }) {
37
33
  } }), _jsx(ExpandButton, { visible: visible, onClick: () => {
38
34
  setVisible(!visible);
39
35
  } })] }), visible ? (_jsx(Select, { multiple: true, native: true, className: classes.select, value: filters.get(column.field) || [], onChange: event => {
40
- faceted.setFilter(column.field, [...event.target.options]
41
- .filter(opt => opt.selected)
42
- .map(opt => opt.value));
36
+ const options = event.target
37
+ .options;
38
+ faceted.setFilter(column.field, [...options].filter(opt => opt.selected).map(opt => opt.value));
43
39
  }, children: vals
44
- .sort((a, b) => a[0].localeCompare(b[0]))
40
+ .toSorted((a, b) => a[0].localeCompare(b[0]))
45
41
  .map(([name, count]) => (_jsxs("option", { value: name, children: [coarseStripHTML(name), " (", count, ")"] }, name))) })) : null] }));
46
42
  });
47
43
  export default FacetFilter;
@@ -6,17 +6,15 @@ const FacetFilters = observer(function FacetFilters({ rows, columns, model, }) {
6
6
  const { faceted } = model;
7
7
  const { filters } = faceted;
8
8
  const facets = columns.slice(1);
9
- const facetFieldToCategoryCountMap = new Map(columns.slice(1).map(f => [f.field, new Map()]));
10
- const filterKeys = faceted.filters.keys();
11
- const facetKeys = facets.map(f => f.field);
9
+ const facetFieldToCategoryCountMap = new Map(facets.map(f => [f.field, new Map()]));
12
10
  const facetKeysPrioritizingUserSelections = new Set();
13
- for (const entry of filterKeys) {
11
+ for (const entry of filters.keys()) {
14
12
  if (filters.get(entry)?.length) {
15
13
  facetKeysPrioritizingUserSelections.add(entry);
16
14
  }
17
15
  }
18
- for (const entry of facetKeys) {
19
- facetKeysPrioritizingUserSelections.add(entry);
16
+ for (const f of facets) {
17
+ facetKeysPrioritizingUserSelections.add(f.field);
20
18
  }
21
19
  let currentRows = rows;
22
20
  for (const facetKey of facetKeysPrioritizingUserSelections) {
@@ -24,21 +22,16 @@ const FacetFilters = observer(function FacetFilters({ rows, columns, model, }) {
24
22
  if (categoryCountMap) {
25
23
  for (const row of currentRows) {
26
24
  const key = getRowStr(facetKey, row);
27
- const currentCount = categoryCountMap.get(key);
28
25
  if (key) {
29
- if (currentCount === undefined) {
30
- categoryCountMap.set(key, 1);
31
- }
32
- else {
33
- categoryCountMap.set(key, currentCount + 1);
34
- }
26
+ categoryCountMap.set(key, (categoryCountMap.get(key) ?? 0) + 1);
35
27
  }
36
28
  }
37
29
  }
38
- const filter = filters.get(facetKey)?.length
39
- ? new Set(filters.get(facetKey))
40
- : undefined;
41
- currentRows = currentRows.filter(row => filter !== undefined ? filter.has(getRowStr(facetKey, row)) : true);
30
+ const filterValues = filters.get(facetKey);
31
+ if (filterValues?.length) {
32
+ const filterSet = new Set(filterValues);
33
+ currentRows = currentRows.filter(row => filterSet.has(getRowStr(facetKey, row)));
34
+ }
42
35
  }
43
36
  return (_jsx("div", { children: facets.map(c => (_jsx(FacetFilter, { vals: [...facetFieldToCategoryCountMap.get(c.field)], column: c, model: model }, c.field))) }));
44
37
  });
@@ -1,9 +1,10 @@
1
1
  import type { HierarchicalTrackSelectorModel } from '../../model.ts';
2
+ import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
3
  import type { GridColDef, GridRowId } from '@mui/x-data-grid';
3
4
  declare const FacetedDataGrid: ({ model, columns, shownTrackIds, selection, }: {
4
5
  model: HierarchicalTrackSelectorModel;
5
6
  columns: GridColDef[];
6
7
  shownTrackIds: Set<GridRowId>;
7
- selection: any[];
8
+ selection: AnyConfigurationModel[];
8
9
  }) => import("react/jsx-runtime").JSX.Element;
9
10
  export default FacetedDataGrid;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useMemo, useState, useTransition } from 'react';
2
+ import { useEffect, useMemo, useState, useTransition } from 'react';
3
3
  import { notEmpty } from '@jbrowse/core/util';
4
4
  import { DataGrid } from '@mui/x-data-grid';
5
5
  import { transaction } from 'mobx';
@@ -10,6 +10,12 @@ const FacetedDataGrid = observer(function FacetedDataGrid({ model, columns, show
10
10
  const { rows, useShoppingCart, showOptions, filteredRows, filteredNonMetadataKeys, filteredMetadataKeys, visible, } = faceted;
11
11
  const [, startTransition] = useTransition();
12
12
  const [widths, setWidths] = useState(() => computeInitialWidths(rows, filteredNonMetadataKeys, filteredMetadataKeys, visible));
13
+ useEffect(() => {
14
+ setWidths(prev => ({
15
+ ...computeInitialWidths(rows, filteredNonMetadataKeys, filteredMetadataKeys, visible),
16
+ ...prev,
17
+ }));
18
+ }, [rows, filteredNonMetadataKeys, filteredMetadataKeys, visible]);
13
19
  const rowSelectionModel = useMemo(() => ({
14
20
  type: 'include',
15
21
  ids: new Set(useShoppingCart ? selection.map(s => s.trackId) : [...shownTrackIds]),
@@ -31,14 +37,17 @@ const FacetedDataGrid = observer(function FacetedDataGrid({ model, columns, show
31
37
  const a1 = shownTrackIds;
32
38
  const a2 = userSelectedIds.ids;
33
39
  transaction(() => {
34
- ;
35
- [...a1].filter(x => !a2.has(x)).map(t => view.hideTrack(t));
36
- [...a2]
37
- .filter(x => !a1.has(x))
38
- .map(t => {
39
- view.showTrack(t);
40
- model.addToRecentlyUsed(t);
41
- });
40
+ for (const t of a1) {
41
+ if (!a2.has(t)) {
42
+ view.hideTrack(t);
43
+ }
44
+ }
45
+ for (const t of a2) {
46
+ if (!a1.has(t)) {
47
+ view.showTrack(t);
48
+ model.addToRecentlyUsed(t);
49
+ }
50
+ }
42
51
  });
43
52
  }
44
53
  else {
@@ -1,4 +1,5 @@
1
1
  import type { HierarchicalTrackSelectorModel } from '../../model.ts';
2
- export default function FacetedHeader({ model, }: {
2
+ declare const FacetedHeader: ({ model, }: {
3
3
  model: HierarchicalTrackSelectorModel;
4
- }): import("react/jsx-runtime").JSX.Element;
4
+ }) => import("react/jsx-runtime").JSX.Element;
5
+ export default FacetedHeader;
@@ -3,13 +3,14 @@ import { useState } from 'react';
3
3
  import { Menu } from '@jbrowse/core/ui';
4
4
  import MoreVert from '@mui/icons-material/MoreVert';
5
5
  import { Grid, IconButton } from '@mui/material';
6
+ import { observer } from 'mobx-react';
6
7
  import ClearableSearchField from "../ClearableSearchField.js";
7
8
  import ShoppingCart from "../ShoppingCart.js";
8
- export default function FacetedHeader({ model, }) {
9
+ const FacetedHeader = observer(function FacetedHeader({ model, }) {
9
10
  const { faceted } = model;
10
11
  const { filterText, showOptions, showFilters, showSparse, useShoppingCart } = faceted;
11
12
  const [anchorEl, setAnchorEl] = useState(null);
12
- return (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: 4, alignItems: "center", children: [_jsx(ClearableSearchField, { label: "Search...", value: filterText, onChange: value => {
13
+ return (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, alignItems: "center", children: [_jsx(ClearableSearchField, { label: "Search...", value: filterText, onChange: value => {
13
14
  faceted.setFilterText(value);
14
15
  } }), _jsx(IconButton, { onClick: event => {
15
16
  setAnchorEl(event.currentTarget);
@@ -52,4 +53,5 @@ export default function FacetedHeader({ model, }) {
52
53
  type: 'checkbox',
53
54
  },
54
55
  ] })] }));
55
- }
56
+ });
57
+ export default FacetedHeader;
@@ -1,10 +1,4 @@
1
1
  import type { HierarchicalTrackSelectorModel } from '../../model.ts';
2
- import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
3
- export interface InfoArgs {
4
- target: HTMLElement;
5
- id: string;
6
- conf: AnyConfigurationModel;
7
- }
8
2
  declare const FacetedSelector: ({ model, }: {
9
3
  model: HierarchicalTrackSelectorModel;
10
4
  }) => import("react/jsx-runtime").JSX.Element;
@@ -21,7 +21,7 @@ const useStyles = makeStyles()({
21
21
  });
22
22
  function HighlightText({ text, query, className, }) {
23
23
  if (!query || !text) {
24
- return _jsx("span", { className: className, children: text });
24
+ return _jsx(SanitizedHTML, { html: text, className: className });
25
25
  }
26
26
  const lowerText = text.toLowerCase();
27
27
  const lowerQuery = query.toLowerCase();
@@ -42,10 +42,14 @@ function HighlightText({ text, query, className, }) {
42
42
  return _jsx("span", { className: className, children: parts });
43
43
  }
44
44
  const frac = 0.75;
45
+ function HighlightCell({ value, filterText, className, }) {
46
+ return value ? (_jsx(HighlightText, { className: className, text: value, query: filterText })) : ('');
47
+ }
45
48
  const FacetedSelector = observer(function FacetedSelector({ model, }) {
46
49
  const { classes } = useStyles();
47
50
  const { selection, shownTrackIds, faceted } = model;
48
51
  const { rows, panelWidth, showFilters, filterText, filteredNonMetadataKeys, filteredMetadataKeys, } = faceted;
52
+ const nonMetadataFieldSet = new Set(['name', ...filteredNonMetadataKeys]);
49
53
  const columns = [
50
54
  {
51
55
  field: 'name',
@@ -53,29 +57,21 @@ const FacetedSelector = observer(function FacetedSelector({ model, }) {
53
57
  renderCell: params => {
54
58
  const { value, row } = params;
55
59
  const { id, conf } = row;
56
- return (_jsxs("div", { className: classes.cell, children: [_jsx(SanitizedHTML, { html: value }), _jsx(TrackSelectorTrackMenu, { id: id, conf: conf, model: model })] }));
60
+ return (_jsxs("div", { className: classes.cell, children: [_jsx(HighlightText, { text: value, query: filterText }), _jsx(TrackSelectorTrackMenu, { id: id, conf: conf, model: model })] }));
57
61
  },
58
62
  },
59
63
  ...filteredNonMetadataKeys.map(e => {
60
64
  return {
61
65
  field: e,
62
- renderCell: params => {
63
- const val = params.value;
64
- return val ? (_jsx(HighlightText, { className: classes.cell, text: val, query: filterText })) : ('');
65
- },
66
+ renderCell: (params) => (_jsx(HighlightCell, { value: params.value, filterText: filterText, className: classes.cell })),
66
67
  };
67
68
  }),
68
69
  ...filteredMetadataKeys.map(e => {
69
70
  return {
70
71
  field: `metadata.${e}`,
71
- headerName: ['name', ...filteredNonMetadataKeys].includes(e)
72
- ? `${e} (from metadata)`
73
- : e,
72
+ headerName: nonMetadataFieldSet.has(e) ? `${e} (from metadata)` : e,
74
73
  valueGetter: (_, row) => `${row.metadata[e] ?? ''}`,
75
- renderCell: params => {
76
- const val = params.value;
77
- return val ? (_jsx(HighlightText, { className: classes.cell, text: val, query: filterText })) : ('');
78
- },
74
+ renderCell: (params) => (_jsx(HighlightCell, { value: params.value, filterText: filterText, className: classes.cell })),
79
75
  };
80
76
  }),
81
77
  ];
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
2
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
+ import useMeasure from '@jbrowse/core/util/useMeasure';
3
5
  import { observer } from 'mobx-react';
4
6
  import ClearableSearchField from "../ClearableSearchField.js";
5
7
  import ShoppingCart from "../ShoppingCart.js";
@@ -13,10 +15,12 @@ const useStyles = makeStyles()(theme => ({
13
15
  }));
14
16
  const HierarchicalTrackSelectorHeader = observer(function HierarchicalTrackSelectorHeader({ model, setHeaderHeight, }) {
15
17
  const { classes } = useStyles();
16
- return (_jsx("div", { ref: ref => {
17
- setHeaderHeight(ref?.getBoundingClientRect().height || 0);
18
- }, "data-testid": "hierarchical_track_selector", children: _jsxs("div", { style: { display: 'flex' }, children: [_jsx(HamburgerMenu, { model: model }), _jsx(ShoppingCart, { model: model }), _jsx(ClearableSearchField, { className: classes.searchBox, label: "Filter tracks", value: model.filterText, onChange: value => {
19
- model.setFilterText(value);
20
- } }), _jsx(RecentlyUsedTracks, { model: model }), _jsx(FavoriteTracks, { model: model })] }) }));
18
+ const [ref, { height }] = useMeasure();
19
+ useEffect(() => {
20
+ if (height !== undefined) {
21
+ setHeaderHeight(height);
22
+ }
23
+ }, [height, setHeaderHeight]);
24
+ return (_jsx("div", { ref: ref, "data-testid": "hierarchical_track_selector", children: _jsxs("div", { style: { display: 'flex' }, children: [_jsx(HamburgerMenu, { model: model }), _jsx(ShoppingCart, { model: model }), _jsx(ClearableSearchField, { className: classes.searchBox, label: "Filter tracks", value: model.filterText, onChange: model.setFilterText }), _jsx(RecentlyUsedTracks, { model: model }), _jsx(FavoriteTracks, { model: model })] }) }));
21
25
  });
22
26
  export default HierarchicalTrackSelectorHeader;
@@ -1,9 +1,12 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { getSession } from '@jbrowse/core/util';
3
4
  import { observer } from 'mobx-react';
5
+ import SharedTooltip from "./SharedTooltip.js";
4
6
  import TreeItem from "./TreeItem.js";
5
7
  const HierarchicalTree = observer(function HierarchicalTree({ height, model, }) {
6
8
  const { flattenedItems, shownTrackIds } = model;
9
+ const { drawerPosition } = getSession(model);
7
10
  const containerRef = useRef(null);
8
11
  const [scrollTop, setScrollTop] = useState(0);
9
12
  const { startIndex, endIndex, totalHeight, itemOffsets } = model.itemOffsets(height, scrollTop);
@@ -12,35 +15,45 @@ const HierarchicalTree = observer(function HierarchicalTree({ height, model, })
12
15
  if (!container) {
13
16
  return;
14
17
  }
18
+ let rafId;
15
19
  const onScroll = () => {
16
- const newScrollTop = container.scrollTop;
17
- setScrollTop(prev => {
18
- const { startIndex: prevStart, endIndex: prevEnd } = model.itemOffsets(height, prev);
19
- const { startIndex: nextStart, endIndex: nextEnd } = model.itemOffsets(height, newScrollTop);
20
- if (prevStart === nextStart && prevEnd === nextEnd) {
21
- return prev;
22
- }
23
- return newScrollTop;
20
+ if (rafId !== undefined) {
21
+ return;
22
+ }
23
+ rafId = requestAnimationFrame(() => {
24
+ rafId = undefined;
25
+ const newScrollTop = container.scrollTop;
26
+ setScrollTop(prev => {
27
+ const { startIndex: prevStart, endIndex: prevEnd } = model.itemOffsets(height, prev);
28
+ const { startIndex: nextStart, endIndex: nextEnd } = model.itemOffsets(height, newScrollTop);
29
+ if (prevStart === nextStart && prevEnd === nextEnd) {
30
+ return prev;
31
+ }
32
+ return newScrollTop;
33
+ });
24
34
  });
25
35
  };
26
36
  container.addEventListener('scroll', onScroll, { passive: true });
27
37
  return () => {
28
38
  container.removeEventListener('scroll', onScroll);
39
+ if (rafId !== undefined) {
40
+ cancelAnimationFrame(rafId);
41
+ }
29
42
  };
30
43
  }, [height, model]);
31
- return (_jsx("div", { ref: containerRef, style: {
44
+ return (_jsxs("div", { ref: containerRef, style: {
32
45
  height,
33
46
  overflowY: 'auto',
34
- }, children: _jsx("div", { style: {
35
- height: totalHeight,
36
- width: '100%',
37
- position: 'relative',
38
- }, children: Array.from({ length: endIndex - startIndex + 1 }, (_, i) => {
39
- const index = startIndex + i;
40
- const item = flattenedItems[index];
41
- return item ? (_jsx(TreeItem, { model: model, item: item, top: itemOffsets[index], checked: item.type === 'track'
42
- ? shownTrackIds.has(item.trackId)
43
- : undefined }, item.id)) : null;
44
- }) }) }));
47
+ }, children: [_jsx("div", { style: {
48
+ height: totalHeight,
49
+ width: '100%',
50
+ position: 'relative',
51
+ }, children: Array.from({ length: endIndex - startIndex + 1 }, (_, i) => {
52
+ const index = startIndex + i;
53
+ const item = flattenedItems[index];
54
+ return item ? (_jsx(TreeItem, { model: model, item: item, top: itemOffsets[index], checked: item.type === 'track'
55
+ ? shownTrackIds.has(item.trackId)
56
+ : undefined }, item.id)) : null;
57
+ }) }), _jsx(SharedTooltip, { containerRef: containerRef, placement: drawerPosition === 'left' ? 'right' : 'left' })] }));
45
58
  });
46
59
  export default HierarchicalTree;
@@ -0,0 +1,5 @@
1
+ import { type RefObject } from 'react';
2
+ export default function SharedTooltip({ containerRef, placement, }: {
3
+ containerRef: RefObject<HTMLDivElement | null>;
4
+ placement: 'left' | 'right';
5
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Tooltip } from '@mui/material';
4
+ export default function SharedTooltip({ containerRef, placement, }) {
5
+ const [state, setState] = useState(null);
6
+ useEffect(() => {
7
+ const container = containerRef.current;
8
+ if (!container) {
9
+ return;
10
+ }
11
+ const handleOver = (e) => {
12
+ const target = e.target.closest('[data-tooltip]');
13
+ if (target) {
14
+ setState({ anchorEl: target, text: target.dataset.tooltip ?? '' });
15
+ }
16
+ };
17
+ const handleOut = (e) => {
18
+ const target = e.target.closest('[data-tooltip]');
19
+ const related = e.relatedTarget?.closest('[data-tooltip]');
20
+ if (target && target !== related) {
21
+ setState(null);
22
+ }
23
+ };
24
+ container.addEventListener('mouseover', handleOver);
25
+ container.addEventListener('mouseout', handleOut);
26
+ return () => {
27
+ container.removeEventListener('mouseover', handleOver);
28
+ container.removeEventListener('mouseout', handleOut);
29
+ };
30
+ }, [containerRef]);
31
+ if (!state?.text) {
32
+ return null;
33
+ }
34
+ return (_jsx(Tooltip, { open: true, title: state.text, placement: placement, slotProps: {
35
+ popper: {
36
+ anchorEl: state.anchorEl,
37
+ },
38
+ }, children: _jsx("span", {}) }));
39
+ }
@@ -19,9 +19,14 @@ const useStyles = makeStyles()(theme => ({
19
19
  }));
20
20
  function getAllSubcategories(node) {
21
21
  const categoryIds = [];
22
- for (const child of node.children) {
23
- if (child.type === 'category') {
24
- categoryIds.push(child.id, ...getAllSubcategories(child));
22
+ const stack = [node];
23
+ while (stack.length > 0) {
24
+ const curr = stack.pop();
25
+ for (const child of curr.children) {
26
+ if (child.type === 'category') {
27
+ categoryIds.push(child.id);
28
+ stack.push(child);
29
+ }
25
30
  }
26
31
  }
27
32
  return categoryIds;
@@ -1,14 +1,8 @@
1
1
  import type { HierarchicalTrackSelectorModel } from '../../model.ts';
2
2
  import type { TreeTrackNode } from '../../types.ts';
3
- import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
4
- export interface InfoArgs {
5
- target: HTMLElement;
6
- id: string;
7
- conf: AnyConfigurationModel;
8
- }
9
- declare const TrackLabel: import("react").NamedExoticComponent<{
3
+ declare const TrackLabel: import("react").MemoExoticComponent<({ model, item, checked, }: {
10
4
  model: HierarchicalTrackSelectorModel;
11
5
  item: TreeTrackNode;
12
6
  checked: boolean;
13
- }>;
7
+ }) => import("react/jsx-runtime").JSX.Element>;
14
8
  export default TrackLabel;
@@ -1,10 +1,8 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { memo, useCallback } from 'react';
3
- import { readConfObject } from '@jbrowse/core/configuration';
4
3
  import SanitizedHTML from '@jbrowse/core/ui/SanitizedHTML';
5
- import { getSession } from '@jbrowse/core/util';
6
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
7
- import { Checkbox, FormControlLabel, Tooltip } from '@mui/material';
5
+ import { Checkbox, FormControlLabel } from '@mui/material';
8
6
  import { observer } from 'mobx-react';
9
7
  import { isUnsupported } from "../util.js";
10
8
  import TrackSelectorTrackMenu from "./TrackSelectorTrackMenu.js";
@@ -35,22 +33,20 @@ const TrackLabelText = observer(function TrackLabelText({ model, conf, id, name,
35
33
  });
36
34
  const TrackLabel = memo(function TrackLabel({ model, item, checked, }) {
37
35
  const { classes } = useStyles();
38
- const { drawerPosition } = getSession(model);
39
- const { id, name, conf, trackId } = item;
40
- const description = readConfObject(conf, 'description');
36
+ const { id, name, conf, trackId, description } = item;
41
37
  const onChange = useCallback(() => {
42
38
  model.view.toggleTrack(trackId);
43
39
  }, [model.view, trackId]);
44
- return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: description, placement: drawerPosition === 'left' ? 'right' : 'left', children: _jsx(FormControlLabel, { className: classes.checkboxLabel, onClick: event => {
45
- if (event.ctrlKey || event.metaKey) {
46
- if (model.selectionSet.has(conf)) {
47
- model.removeFromSelection([conf]);
48
- }
49
- else {
50
- model.addToSelection([conf]);
51
- }
52
- event.preventDefault();
40
+ return (_jsxs(_Fragment, { children: [_jsx(FormControlLabel, { className: classes.checkboxLabel, "data-tooltip": description || undefined, "aria-description": description || undefined, onClick: event => {
41
+ if (event.ctrlKey || event.metaKey) {
42
+ if (model.selectionSet.has(conf)) {
43
+ model.removeFromSelection([conf]);
53
44
  }
54
- }, control: _jsx(TrackCheckbox, { checked: checked, onChange: onChange, id: id, disabled: isUnsupported(name), className: classes.compactCheckbox }), label: _jsx(TrackLabelText, { model: model, conf: conf, id: id, name: name, selectedClass: classes.selected }) }) }), _jsx(TrackSelectorTrackMenu, { model: model, id: id, conf: conf })] }));
45
+ else {
46
+ model.addToSelection([conf]);
47
+ }
48
+ event.preventDefault();
49
+ }
50
+ }, control: _jsx(TrackCheckbox, { checked: checked, onChange: onChange, id: id, disabled: isUnsupported(name), className: classes.compactCheckbox }), label: _jsx(TrackLabelText, { model: model, conf: conf, id: id, name: name, selectedClass: classes.selected }) }), _jsx(TrackSelectorTrackMenu, { model: model, id: id, conf: conf })] }));
55
51
  });
56
52
  export default TrackLabel;
@@ -1,9 +1,9 @@
1
1
  import type { HierarchicalTrackSelectorModel } from '../../model.ts';
2
2
  import type { TreeNode } from '../../types.ts';
3
- declare const TreeItem: import("react").NamedExoticComponent<{
3
+ declare const TreeItem: import("react").MemoExoticComponent<({ item, model, top, checked, }: {
4
4
  item: TreeNode;
5
5
  model: HierarchicalTrackSelectorModel;
6
6
  top: number;
7
7
  checked?: boolean;
8
- }>;
8
+ }) => import("react/jsx-runtime").JSX.Element>;
9
9
  export default TreeItem;
@@ -26,6 +26,22 @@ export declare function facetedStateTreeF(): import("@jbrowse/mobx-state-tree").
26
26
  [x: string]: any;
27
27
  } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
28
28
  } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>)[];
29
+ } & {
30
+ readonly allRows: {
31
+ readonly id: string;
32
+ readonly conf: {
33
+ [x: string]: any;
34
+ } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & {
35
+ setSubschema(slotName: string, data: Record<string, unknown>): Record<string, unknown> | ({
36
+ [x: string]: any;
37
+ } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
38
+ } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>;
39
+ readonly name: string;
40
+ readonly category: any;
41
+ readonly adapter: string;
42
+ readonly description: string | undefined;
43
+ readonly metadata: Record<string, unknown>;
44
+ }[];
29
45
  } & {
30
46
  readonly rows: {
31
47
  readonly id: string;
@@ -37,9 +53,9 @@ export declare function facetedStateTreeF(): import("@jbrowse/mobx-state-tree").
37
53
  } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
38
54
  } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>;
39
55
  readonly name: string;
40
- readonly category: string;
56
+ readonly category: any;
41
57
  readonly adapter: string;
42
- readonly description: string;
58
+ readonly description: string | undefined;
43
59
  readonly metadata: Record<string, unknown>;
44
60
  }[];
45
61
  } & {
@@ -57,9 +73,9 @@ export declare function facetedStateTreeF(): import("@jbrowse/mobx-state-tree").
57
73
  } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
58
74
  } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>;
59
75
  readonly name: string;
60
- readonly category: string;
76
+ readonly category: any;
61
77
  readonly adapter: string;
62
- readonly description: string;
78
+ readonly description: string | undefined;
63
79
  readonly metadata: Record<string, unknown>;
64
80
  }[];
65
81
  } & {
@@ -5,7 +5,6 @@ import { addDisposer, getParent, types } from '@jbrowse/mobx-state-tree';
5
5
  import { autorun, observable } from 'mobx';
6
6
  import { getRowStr } from "./components/faceted/util.js";
7
7
  import { findNonSparseKeys, getRootKeys } from "./facetedUtil.js";
8
- import { matches, matchesMetadata } from "./util.js";
9
8
  const nonMetadataKeys = ['category', 'adapter', 'description'];
10
9
  export function facetedStateTreeF() {
11
10
  return types
@@ -54,36 +53,48 @@ export function facetedStateTreeF() {
54
53
  },
55
54
  }))
56
55
  .views(self => ({
57
- get rows() {
56
+ get allRows() {
58
57
  const session = getSession(self);
59
- const { allTrackConfigurations, filterText } = self;
60
- return allTrackConfigurations
61
- .filter(conf => matches(filterText, conf, session) ||
62
- matchesMetadata(filterText, conf))
63
- .map(track => ({
58
+ return self.allTrackConfigurations.map(track => ({
64
59
  id: track.trackId,
65
60
  conf: track,
66
61
  name: getTrackName(track, session),
67
62
  category: readConfObject(track, 'category')?.join(', '),
68
63
  adapter: readConfObject(track, 'adapter')?.type,
69
64
  description: readConfObject(track, 'description'),
70
- metadata: (track.metadata || {}),
65
+ metadata: (readConfObject(track, 'metadata') || {}),
71
66
  }));
72
67
  },
68
+ }))
69
+ .views(self => ({
70
+ get rows() {
71
+ const queryLower = self.filterText.toLowerCase();
72
+ if (!queryLower) {
73
+ return self.allRows;
74
+ }
75
+ return self.allRows.filter(row => row.name.toLowerCase().includes(queryLower) ||
76
+ row.category?.toLowerCase().includes(queryLower) ||
77
+ row.description?.toLowerCase().includes(queryLower) ||
78
+ Object.values(row.metadata).some(v => v !== null &&
79
+ v !== undefined &&
80
+ `${v}`.toLowerCase().includes(queryLower)));
81
+ },
73
82
  }))
74
83
  .views(self => ({
75
84
  get filteredNonMetadataKeys() {
76
85
  return self.showSparse
77
86
  ? nonMetadataKeys
78
- : findNonSparseKeys(nonMetadataKeys, self.rows, (r, f) => r[f]);
87
+ : findNonSparseKeys(nonMetadataKeys, self.allRows, (r, f) => r[f]);
79
88
  },
80
89
  get metadataKeys() {
81
- return [...new Set(self.rows.flatMap(row => getRootKeys(row.metadata)))];
90
+ return [
91
+ ...new Set(self.allRows.flatMap(row => getRootKeys(row.metadata))),
92
+ ];
82
93
  },
83
94
  get filteredMetadataKeys() {
84
95
  return self.showSparse
85
96
  ? this.metadataKeys
86
- : findNonSparseKeys(this.metadataKeys, self.rows, (r, f) => r.metadata[f]);
97
+ : findNonSparseKeys(this.metadataKeys, self.allRows, (r, f) => r.metadata[f]);
87
98
  },
88
99
  get fields() {
89
100
  return [
@@ -1,2 +1,2 @@
1
- export declare function findNonSparseKeys(keys: readonly string[], rows: Record<string, unknown>[], cb: (row: Record<string, unknown>, f: string) => unknown): string[];
1
+ export declare function findNonSparseKeys<T>(keys: readonly string[], rows: T[], cb: (row: T, f: string) => unknown, threshold?: number): string[];
2
2
  export declare function getRootKeys(obj: Record<string, unknown>): string[];
@@ -1,8 +1,20 @@
1
- export function findNonSparseKeys(keys, rows, cb) {
2
- return keys.filter(key => rows.filter(row => cb(row, key)).length > 5);
1
+ export function findNonSparseKeys(keys, rows, cb, threshold = 5) {
2
+ return keys.filter(key => {
3
+ let count = 0;
4
+ for (const row of rows) {
5
+ if (cb(row, key)) {
6
+ count++;
7
+ if (count > threshold) {
8
+ return true;
9
+ }
10
+ }
11
+ }
12
+ return false;
13
+ });
3
14
  }
4
15
  export function getRootKeys(obj) {
5
- return Object.entries(obj)
6
- .map(([key, val]) => (typeof val === 'string' ? key : ''))
7
- .filter((f) => !!f);
16
+ return Object.keys(obj).filter(key => {
17
+ const val = obj[key];
18
+ return val !== null && val !== undefined && typeof val !== 'object';
19
+ });
8
20
  }
@@ -51,6 +51,7 @@ export function generateHierarchy({ model, trackConfs, extra, noCategories, menu
51
51
  id: extra ? `${extra},${conf.trackId}` : conf.trackId,
52
52
  trackId: conf.trackId,
53
53
  name: getTrackName(conf, session),
54
+ description: readConfObject(conf, 'description') || '',
54
55
  conf,
55
56
  children: [],
56
57
  nestingLevel: nestingLevel + 1,
@@ -37,6 +37,22 @@ export default function stateTreeFactory(pluginManager: PluginManager): import("
37
37
  [x: string]: any;
38
38
  } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
39
39
  } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>)[];
40
+ } & {
41
+ readonly allRows: {
42
+ readonly id: string;
43
+ readonly conf: {
44
+ [x: string]: any;
45
+ } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & {
46
+ setSubschema(slotName: string, data: Record<string, unknown>): Record<string, unknown> | ({
47
+ [x: string]: any;
48
+ } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
49
+ } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>;
50
+ readonly name: string;
51
+ readonly category: any;
52
+ readonly adapter: string;
53
+ readonly description: string | undefined;
54
+ readonly metadata: Record<string, unknown>;
55
+ }[];
40
56
  } & {
41
57
  readonly rows: {
42
58
  readonly id: string;
@@ -48,9 +64,9 @@ export default function stateTreeFactory(pluginManager: PluginManager): import("
48
64
  } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
49
65
  } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>;
50
66
  readonly name: string;
51
- readonly category: string;
67
+ readonly category: any;
52
68
  readonly adapter: string;
53
- readonly description: string;
69
+ readonly description: string | undefined;
54
70
  readonly metadata: Record<string, unknown>;
55
71
  }[];
56
72
  } & {
@@ -68,9 +84,9 @@ export default function stateTreeFactory(pluginManager: PluginManager): import("
68
84
  } & import("@jbrowse/mobx-state-tree/dist/internal").NonEmptyObject & any & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>);
69
85
  } & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/core/configuration").AnyConfigurationSchemaType>;
70
86
  readonly name: string;
71
- readonly category: string;
87
+ readonly category: any;
72
88
  readonly adapter: string;
73
- readonly description: string;
89
+ readonly description: string | undefined;
74
90
  readonly metadata: Record<string, unknown>;
75
91
  }[];
76
92
  } & {
@@ -287,11 +287,12 @@ export default function stateTreeFactory(pluginManager) {
287
287
  return flatten(self.hierarchy.children);
288
288
  },
289
289
  get flattenedItemOffsets() {
290
+ const items = this.flattenedItems;
290
291
  const offsets = [];
291
292
  let cumulativeHeight = 0;
292
- for (let i = 0, l = this.flattenedItems.length; i < l; i++) {
293
+ for (let i = 0, l = items.length; i < l; i++) {
293
294
  offsets.push(cumulativeHeight);
294
- cumulativeHeight += getItemHeight(this.flattenedItems[i]);
295
+ cumulativeHeight += getItemHeight(items[i]);
295
296
  }
296
297
  return { cumulativeHeight, offsets };
297
298
  },
@@ -4,6 +4,7 @@ export interface TreeTrackNode {
4
4
  id: string;
5
5
  trackId: string;
6
6
  conf: AnyConfigurationModel;
7
+ description: string;
7
8
  children: TreeNode[];
8
9
  nestingLevel: number;
9
10
  type: 'track';
@@ -22,9 +22,11 @@ export function matches(query, conf, session) {
22
22
  export function matchesMetadata(query, conf) {
23
23
  const queryLower = query.toLowerCase();
24
24
  const description = (readConfObject(conf, 'description') || '');
25
- const metadata = (conf.metadata || {});
25
+ const metadata = (readConfObject(conf, 'metadata') || {});
26
26
  return (description.toLowerCase().includes(queryLower) ||
27
- Object.values(metadata).some(v => typeof v === 'string' && v.toLowerCase().includes(queryLower)));
27
+ Object.values(metadata).some(v => v !== null &&
28
+ v !== undefined &&
29
+ `${v}`.toLowerCase().includes(queryLower)));
28
30
  }
29
31
  export function findSubCategories(obj, paths, depth = 0) {
30
32
  let hasSubs = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-data-management",
3
- "version": "4.1.13",
3
+ "version": "4.1.15",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 linear genome view",
6
6
  "keywords": [
@@ -22,16 +22,16 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@gmod/ucsc-hub": "^2.0.3",
25
- "@jbrowse/mobx-state-tree": "^5.5.0",
26
- "@mui/icons-material": "^7.3.8",
27
- "@mui/material": "^7.3.8",
28
- "@mui/x-data-grid": "^8.27.1",
25
+ "@jbrowse/mobx-state-tree": "^5.6.0",
26
+ "@mui/icons-material": "^7.3.9",
27
+ "@mui/material": "^7.3.9",
28
+ "@mui/x-data-grid": "^8.28.2",
29
29
  "deepmerge": "^4.3.1",
30
30
  "mobx": "^6.15.0",
31
31
  "mobx-react": "^9.2.1",
32
- "@jbrowse/core": "^4.1.13",
33
- "@jbrowse/plugin-config": "^4.1.13",
34
- "@jbrowse/product-core": "^4.1.13"
32
+ "@jbrowse/product-core": "^4.1.15",
33
+ "@jbrowse/core": "^4.1.15",
34
+ "@jbrowse/plugin-config": "^4.1.15"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=18.0.0"
@@ -40,6 +40,9 @@
40
40
  "access": "public"
41
41
  },
42
42
  "sideEffects": false,
43
+ "devDependencies": {
44
+ "@jbrowse/app-core": "4.1.15"
45
+ },
43
46
  "scripts": {
44
47
  "build": "pnpm run /^build:/",
45
48
  "test": "cd ../..; jest --passWithNoTests plugins/data-management",