@jbrowse/plugin-data-management 4.1.14 → 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.
@@ -1,21 +1,22 @@
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
+ setLocalValue(event.target.value);
19
20
  }, slotProps: {
20
21
  input: {
21
22
  endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: () => {
@@ -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;
@@ -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,
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-data-management",
3
- "version": "4.1.14",
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.3",
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.14",
33
- "@jbrowse/plugin-config": "^4.1.14",
34
- "@jbrowse/product-core": "^4.1.14"
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",