@weng-lab/genomebrowser-ui 0.1.12 → 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.local +1 -0
- package/dist/TrackSelect/DataGrid/DefaultGroupingCell.d.ts +6 -0
- package/dist/TrackSelect/FolderList/Breadcrumb.d.ts +6 -0
- package/dist/TrackSelect/FolderList/FolderCard.d.ts +6 -0
- package/dist/TrackSelect/FolderList/FolderList.d.ts +6 -0
- package/dist/TrackSelect/{Data/humanBiosamples.json.d.ts → Folders/biosamples/data/human.json.d.ts} +1940 -1919
- package/dist/TrackSelect/{Data/mouseBiosamples.json.d.ts → Folders/biosamples/data/mouse.json.d.ts} +408 -357
- package/dist/TrackSelect/Folders/biosamples/human.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/mouse.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +14 -0
- package/dist/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.d.ts +6 -0
- package/dist/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/shared/columns.d.ts +14 -0
- package/dist/TrackSelect/Folders/biosamples/shared/constants.d.ts +19 -0
- package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +24 -0
- package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +25 -0
- package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +44 -0
- package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +10 -0
- package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +10 -0
- package/dist/TrackSelect/Folders/genes/human.d.ts +7 -0
- package/dist/TrackSelect/Folders/genes/mouse.d.ts +7 -0
- package/dist/TrackSelect/Folders/genes/shared/columns.d.ts +14 -0
- package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +12 -0
- package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +13 -0
- package/dist/TrackSelect/Folders/genes/shared/types.d.ts +26 -0
- package/dist/TrackSelect/Folders/index.d.ts +16 -0
- package/dist/TrackSelect/Folders/types.d.ts +76 -0
- package/dist/TrackSelect/TrackSelect.d.ts +12 -5
- package/dist/TrackSelect/TreeView/CustomTreeItem.d.ts +3 -0
- package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
- package/dist/TrackSelect/store.d.ts +1 -2
- package/dist/TrackSelect/types.d.ts +24 -62
- package/dist/genomebrowser-ui.es.js +1373 -2117
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/dist/lib.d.ts +3 -2
- package/package.json +3 -2
- package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +36 -20
- package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -0
- package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -0
- package/src/TrackSelect/FolderList/FolderCard.tsx +51 -0
- package/src/TrackSelect/FolderList/FolderList.tsx +47 -0
- package/src/TrackSelect/Folders/NEW.md +929 -0
- package/src/TrackSelect/{Data → Folders/biosamples/data}/formatBiosamples.go +2 -2
- package/src/TrackSelect/{Data/humanBiosamples.json → Folders/biosamples/data/human.json} +1940 -1919
- package/src/TrackSelect/{Data/mouseBiosamples.json → Folders/biosamples/data/mouse.json} +408 -357
- package/src/TrackSelect/Folders/biosamples/human.ts +17 -0
- package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -0
- package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +65 -0
- package/src/TrackSelect/{DataGrid/GroupingCell.tsx → Folders/biosamples/shared/BiosampleGroupingCell.tsx} +7 -5
- package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -0
- package/src/TrackSelect/{DataGrid → Folders/biosamples/shared}/columns.tsx +31 -17
- package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -0
- package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -0
- package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +227 -0
- package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -0
- package/src/TrackSelect/Folders/genes/data/human.json +7 -0
- package/src/TrackSelect/Folders/genes/data/mouse.json +7 -0
- package/src/TrackSelect/Folders/genes/human.ts +16 -0
- package/src/TrackSelect/Folders/genes/mouse.ts +16 -0
- package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -0
- package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -0
- package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -0
- package/src/TrackSelect/Folders/genes/shared/types.ts +29 -0
- package/src/TrackSelect/Folders/index.ts +30 -0
- package/src/TrackSelect/Folders/types.ts +95 -0
- package/src/TrackSelect/TrackSelect.tsx +409 -311
- package/src/TrackSelect/TreeView/CustomTreeItem.tsx +217 -0
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +47 -42
- package/src/TrackSelect/store.ts +103 -46
- package/src/TrackSelect/types.ts +28 -74
- package/src/lib.ts +4 -2
- package/test/main.tsx +112 -168
- package/.claude/settings.local.json +0 -7
- package/dist/TrackSelect/DataGrid/CustomToolbar.d.ts +0 -12
- package/dist/TrackSelect/DataGrid/GroupingCell.d.ts +0 -2
- package/dist/TrackSelect/DataGrid/columns.d.ts +0 -4
- package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +0 -49
- package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +0 -49
- package/dist/TrackSelect/consts.d.ts +0 -11
- package/src/TrackSelect/DataGrid/CustomToolbar.tsx +0 -152
- package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +0 -155
- package/src/TrackSelect/TreeView/treeViewHelpers.tsx +0 -475
- package/src/TrackSelect/consts.ts +0 -92
- package/src/TrackSelect/issues.md +0 -404
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import Folder from "@mui/icons-material/Folder";
|
|
2
|
+
import IndeterminateCheckBoxRoundedIcon from "@mui/icons-material/IndeterminateCheckBoxRounded";
|
|
3
|
+
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
|
4
|
+
import Collapse from "@mui/material/Collapse";
|
|
5
|
+
import { alpha, styled } from "@mui/material/styles";
|
|
6
|
+
import {
|
|
7
|
+
TreeItemCheckbox,
|
|
8
|
+
TreeItemIconContainer,
|
|
9
|
+
TreeItemLabel,
|
|
10
|
+
} from "@mui/x-tree-view/TreeItem";
|
|
11
|
+
import { TreeItemIcon } from "@mui/x-tree-view/TreeItemIcon";
|
|
12
|
+
import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
|
|
13
|
+
import { useTreeItemModel } from "@mui/x-tree-view/hooks";
|
|
14
|
+
import { useTreeItem } from "@mui/x-tree-view/useTreeItem";
|
|
15
|
+
import React, { ReactNode } from "react";
|
|
16
|
+
import {
|
|
17
|
+
CustomLabelProps,
|
|
18
|
+
CustomTreeItemProps,
|
|
19
|
+
ExtendedTreeItemProps,
|
|
20
|
+
} from "../types";
|
|
21
|
+
|
|
22
|
+
// Everything below is styling for the custom directory look of the tree view
|
|
23
|
+
const TreeItemRoot = styled("li")(({ theme }) => ({
|
|
24
|
+
listStyle: "none",
|
|
25
|
+
margin: 0,
|
|
26
|
+
padding: 0,
|
|
27
|
+
outline: 4,
|
|
28
|
+
color: theme.palette.grey[400],
|
|
29
|
+
...theme.applyStyles("light", {
|
|
30
|
+
color: theme.palette.grey[600], // controls colors of the MUI icons
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const TreeItemLabelText = styled(Typography)({
|
|
35
|
+
color: "black",
|
|
36
|
+
fontFamily: "inherit",
|
|
37
|
+
overflow: "hidden",
|
|
38
|
+
textOverflow: "ellipsis",
|
|
39
|
+
whiteSpace: "nowrap",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function CustomLabel({
|
|
43
|
+
icon: Icon,
|
|
44
|
+
children,
|
|
45
|
+
isAssayItem,
|
|
46
|
+
assayName,
|
|
47
|
+
renderIcon,
|
|
48
|
+
...other
|
|
49
|
+
}: CustomLabelProps) {
|
|
50
|
+
const variant = isAssayItem ? "subtitle2" : "body2";
|
|
51
|
+
const fontWeight = isAssayItem ? "bold" : 500;
|
|
52
|
+
const labelText = typeof children === "string" ? children : "";
|
|
53
|
+
return (
|
|
54
|
+
<TreeItemLabel
|
|
55
|
+
{...other}
|
|
56
|
+
sx={{
|
|
57
|
+
display: "flex",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
minWidth: 0,
|
|
60
|
+
overflow: "hidden",
|
|
61
|
+
flex: 1,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{Icon && React.isValidElement(Icon) ? (
|
|
65
|
+
<Box className="labelIcon" sx={{ mr: 1, flexShrink: 0 }}>
|
|
66
|
+
{Icon}
|
|
67
|
+
</Box>
|
|
68
|
+
) : (
|
|
69
|
+
<Box
|
|
70
|
+
component={Icon as React.ElementType}
|
|
71
|
+
className="labelIcon"
|
|
72
|
+
color="inherit"
|
|
73
|
+
sx={{ mr: 1, fontSize: "1.2rem", flexShrink: 0 }}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
<Stack
|
|
77
|
+
direction="row"
|
|
78
|
+
spacing={1}
|
|
79
|
+
alignItems="center"
|
|
80
|
+
sx={{ minWidth: 0, overflow: "hidden", flex: 1 }}
|
|
81
|
+
>
|
|
82
|
+
{isAssayItem && renderIcon && (
|
|
83
|
+
<Box sx={{ flexShrink: 0 }}>{renderIcon(other.id)}</Box>
|
|
84
|
+
)}
|
|
85
|
+
{assayName && renderIcon && (
|
|
86
|
+
<Box sx={{ flexShrink: 0 }}>{renderIcon(assayName)}</Box>
|
|
87
|
+
)}
|
|
88
|
+
<Tooltip title={labelText} enterDelay={500} placement="top">
|
|
89
|
+
<TreeItemLabelText fontWeight={fontWeight} variant={variant}>
|
|
90
|
+
{labelText}
|
|
91
|
+
</TreeItemLabelText>
|
|
92
|
+
</Tooltip>
|
|
93
|
+
</Stack>
|
|
94
|
+
</TreeItemLabel>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const TreeItemContent = styled("div")(({ theme }) => ({
|
|
99
|
+
padding: theme.spacing(0.5),
|
|
100
|
+
paddingRight: theme.spacing(2),
|
|
101
|
+
paddingLeft: `calc(${theme.spacing(1)} + var(--TreeView-itemChildrenIndentation) * var(--TreeView-itemDepth))`,
|
|
102
|
+
width: "100%",
|
|
103
|
+
boxSizing: "border-box", // prevent width + padding to overflow
|
|
104
|
+
position: "relative",
|
|
105
|
+
display: "flex",
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
gap: theme.spacing(1),
|
|
108
|
+
cursor: "pointer",
|
|
109
|
+
WebkitTapHighlightColor: "transparent",
|
|
110
|
+
flexDirection: "row-reverse",
|
|
111
|
+
borderRadius: theme.spacing(0.7),
|
|
112
|
+
marginBottom: theme.spacing(0.5),
|
|
113
|
+
marginTop: theme.spacing(0.5),
|
|
114
|
+
fontWeight: 500,
|
|
115
|
+
"&:hover": {
|
|
116
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.1),
|
|
117
|
+
color: "white",
|
|
118
|
+
...theme.applyStyles("light", {
|
|
119
|
+
color: theme.palette.primary.main,
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
const getIconFromTreeItemType = (
|
|
125
|
+
itemType: string,
|
|
126
|
+
renderIcon?: (name: string) => ReactNode,
|
|
127
|
+
) => {
|
|
128
|
+
switch (itemType) {
|
|
129
|
+
case "folder":
|
|
130
|
+
return Folder;
|
|
131
|
+
case "removeable":
|
|
132
|
+
return IndeterminateCheckBoxRoundedIcon;
|
|
133
|
+
default:
|
|
134
|
+
return renderIcon ? renderIcon(itemType) : Folder;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
|
|
139
|
+
props: CustomTreeItemProps,
|
|
140
|
+
ref: React.Ref<HTMLLIElement>,
|
|
141
|
+
) {
|
|
142
|
+
const {
|
|
143
|
+
id,
|
|
144
|
+
itemId,
|
|
145
|
+
label,
|
|
146
|
+
disabled,
|
|
147
|
+
children,
|
|
148
|
+
onRemove,
|
|
149
|
+
renderIcon,
|
|
150
|
+
...other
|
|
151
|
+
} = props;
|
|
152
|
+
|
|
153
|
+
const {
|
|
154
|
+
getContextProviderProps,
|
|
155
|
+
getRootProps,
|
|
156
|
+
getContentProps,
|
|
157
|
+
getIconContainerProps,
|
|
158
|
+
getCheckboxProps,
|
|
159
|
+
getLabelProps,
|
|
160
|
+
getGroupTransitionProps,
|
|
161
|
+
status,
|
|
162
|
+
} = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
|
|
163
|
+
|
|
164
|
+
const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
|
|
165
|
+
const icon = getIconFromTreeItemType(item.icon, renderIcon);
|
|
166
|
+
|
|
167
|
+
const handleRemoveIconClick = (e: React.MouseEvent) => {
|
|
168
|
+
e.stopPropagation(); // prevent item expand/select
|
|
169
|
+
onRemove?.(item);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<TreeItemProvider {...getContextProviderProps()}>
|
|
174
|
+
<TreeItemRoot {...getRootProps(other)}>
|
|
175
|
+
<TreeItemContent {...getContentProps()}>
|
|
176
|
+
<TreeItemIconContainer {...getIconContainerProps()}>
|
|
177
|
+
<TreeItemIcon status={status} />
|
|
178
|
+
</TreeItemIconContainer>
|
|
179
|
+
<TreeItemCheckbox {...getCheckboxProps()} />
|
|
180
|
+
<CustomLabel
|
|
181
|
+
{...getLabelProps({
|
|
182
|
+
icon:
|
|
183
|
+
item.icon === "removeable" ? (
|
|
184
|
+
<Box
|
|
185
|
+
onClick={handleRemoveIconClick}
|
|
186
|
+
sx={{
|
|
187
|
+
width: 20,
|
|
188
|
+
height: 20,
|
|
189
|
+
display: "flex",
|
|
190
|
+
alignItems: "center",
|
|
191
|
+
justifyContent: "center",
|
|
192
|
+
borderRadius: "4px",
|
|
193
|
+
cursor: "pointer",
|
|
194
|
+
mr: 1,
|
|
195
|
+
"&:hover": {
|
|
196
|
+
backgroundColor: "rgba(0,0,0,0.1)",
|
|
197
|
+
},
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<IndeterminateCheckBoxRoundedIcon fontSize="small" />
|
|
201
|
+
</Box>
|
|
202
|
+
) : (
|
|
203
|
+
icon
|
|
204
|
+
),
|
|
205
|
+
expandable: (status.expandable && status.expanded).toString(),
|
|
206
|
+
isAssayItem: item.isAssayItem,
|
|
207
|
+
assayName: item.assayName,
|
|
208
|
+
id: item.id,
|
|
209
|
+
renderIcon,
|
|
210
|
+
})}
|
|
211
|
+
/>
|
|
212
|
+
</TreeItemContent>
|
|
213
|
+
{children && <Collapse {...getGroupTransitionProps()} />}
|
|
214
|
+
</TreeItemRoot>
|
|
215
|
+
</TreeItemProvider>
|
|
216
|
+
);
|
|
217
|
+
});
|
|
@@ -1,50 +1,61 @@
|
|
|
1
|
-
import { Box, Paper, Typography } from "@mui/material";
|
|
1
|
+
import { Avatar, Box, Paper, Typography } from "@mui/material";
|
|
2
2
|
import { RichTreeView, TreeViewBaseItem } from "@mui/x-tree-view";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
4
|
import {
|
|
4
5
|
CustomTreeItemProps,
|
|
5
6
|
ExtendedTreeItemProps,
|
|
6
7
|
TreeViewWrapperProps,
|
|
7
8
|
} from "../types";
|
|
8
|
-
import { CustomTreeItem } from "./
|
|
9
|
-
|
|
9
|
+
import { CustomTreeItem } from "./CustomTreeItem";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively collects all item IDs that have children (expandable items)
|
|
13
|
+
*/
|
|
14
|
+
function getAllExpandableItemIds(
|
|
15
|
+
items: TreeViewBaseItem<ExtendedTreeItemProps>[],
|
|
16
|
+
): string[] {
|
|
17
|
+
const ids: string[] = [];
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
if (item.children && item.children.length > 0) {
|
|
20
|
+
ids.push(item.id);
|
|
21
|
+
ids.push(...getAllExpandableItemIds(item.children));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return ids;
|
|
25
|
+
}
|
|
10
26
|
|
|
11
27
|
export function TreeViewWrapper({
|
|
12
|
-
store,
|
|
13
28
|
items,
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
selectedCount,
|
|
30
|
+
onRemove,
|
|
31
|
+
TreeItemComponent,
|
|
16
32
|
}: TreeViewWrapperProps) {
|
|
17
|
-
const
|
|
18
|
-
|
|
33
|
+
const allExpandableIds = useMemo(
|
|
34
|
+
() => getAllExpandableItemIds(items),
|
|
35
|
+
[items],
|
|
36
|
+
);
|
|
37
|
+
const [expandedItems, setExpandedItems] =
|
|
38
|
+
useState<string[]>(allExpandableIds);
|
|
39
|
+
|
|
40
|
+
// Auto-expand new items when they're added
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setExpandedItems((prev) => {
|
|
43
|
+
const newIds = allExpandableIds.filter((id) => !prev.includes(id));
|
|
44
|
+
if (newIds.length > 0) {
|
|
45
|
+
return [...prev, ...newIds];
|
|
46
|
+
}
|
|
47
|
+
return prev;
|
|
48
|
+
});
|
|
49
|
+
}, [allExpandableIds]);
|
|
19
50
|
|
|
20
51
|
const handleRemoveTreeItem = (
|
|
21
52
|
item: TreeViewBaseItem<ExtendedTreeItemProps>,
|
|
22
53
|
) => {
|
|
23
|
-
|
|
24
|
-
if (removedIds && removedIds.length) {
|
|
25
|
-
const idsToRemove = new Set(removedIds);
|
|
26
|
-
|
|
27
|
-
// Also remove any auto-generated group IDs that contain these tracks
|
|
28
|
-
removedIds.forEach((id) => {
|
|
29
|
-
const row = rowById.get(id);
|
|
30
|
-
if (row) {
|
|
31
|
-
// Add the auto-generated group IDs for this track's grouping hierarchy
|
|
32
|
-
// Default view: ontology -> displayname
|
|
33
|
-
idsToRemove.add(`auto-generated-row-ontology/${row.ontology}`);
|
|
34
|
-
idsToRemove.add(
|
|
35
|
-
`auto-generated-row-ontology/${row.ontology}-displayname/${row.displayname}`,
|
|
36
|
-
);
|
|
37
|
-
// Sorted by assay view: assay -> ontology -> displayname
|
|
38
|
-
idsToRemove.add(`auto-generated-row-assay/${row.assay}`);
|
|
39
|
-
idsToRemove.add(
|
|
40
|
-
`auto-generated-row-assay/${row.assay}-ontology/${row.ontology}`,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
removeIds(idsToRemove);
|
|
45
|
-
}
|
|
54
|
+
onRemove(item);
|
|
46
55
|
};
|
|
47
56
|
|
|
57
|
+
const TreeItem = TreeItemComponent ?? CustomTreeItem;
|
|
58
|
+
|
|
48
59
|
return (
|
|
49
60
|
<Paper
|
|
50
61
|
sx={{
|
|
@@ -78,16 +89,9 @@ export function TreeViewWrapper({
|
|
|
78
89
|
color: "text.primary",
|
|
79
90
|
}}
|
|
80
91
|
>
|
|
81
|
-
{
|
|
92
|
+
{selectedCount}
|
|
82
93
|
</Avatar>
|
|
83
|
-
<Typography fontWeight="bold">
|
|
84
|
-
Active Tracks
|
|
85
|
-
{isSearchResult && (
|
|
86
|
-
<Typography component="span" color="text.secondary" sx={{ ml: 1 }}>
|
|
87
|
-
({items[0].allRowInfo!.length} search results)
|
|
88
|
-
</Typography>
|
|
89
|
-
)}
|
|
90
|
-
</Typography>
|
|
94
|
+
<Typography fontWeight="bold">Active Tracks</Typography>
|
|
91
95
|
</Box>
|
|
92
96
|
<Box
|
|
93
97
|
sx={{
|
|
@@ -97,8 +101,9 @@ export function TreeViewWrapper({
|
|
|
97
101
|
>
|
|
98
102
|
<RichTreeView
|
|
99
103
|
items={items}
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
expandedItems={expandedItems}
|
|
105
|
+
onExpandedItemsChange={(_event, ids) => setExpandedItems(ids)}
|
|
106
|
+
slots={{ item: TreeItem }}
|
|
102
107
|
slotProps={{
|
|
103
108
|
item: {
|
|
104
109
|
onRemove: handleRemoveTreeItem,
|
package/src/TrackSelect/store.ts
CHANGED
|
@@ -1,60 +1,117 @@
|
|
|
1
1
|
import { create, StoreApi, UseBoundStore } from "zustand";
|
|
2
|
-
import {
|
|
3
|
-
import { RowInfo, SelectionAction, SelectionState } from "./types";
|
|
2
|
+
import { SelectionAction, SelectionState } from "./types";
|
|
4
3
|
|
|
5
4
|
export type SelectionStoreInstance = UseBoundStore<
|
|
6
5
|
StoreApi<SelectionState & SelectionAction>
|
|
7
6
|
>;
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const DEFAULT_STORAGE_KEY = "trackSelect_selection";
|
|
9
|
+
|
|
10
|
+
type SerializedSelection = Record<string, string[]>;
|
|
11
|
+
|
|
12
|
+
const serializeSelection = (
|
|
13
|
+
selection: Map<string, Set<string>>,
|
|
14
|
+
): SerializedSelection => {
|
|
15
|
+
const obj: SerializedSelection = {};
|
|
16
|
+
selection.forEach((ids, folderId) => {
|
|
17
|
+
obj[folderId] = Array.from(ids);
|
|
18
|
+
});
|
|
19
|
+
return obj;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const deserializeSelection = (
|
|
23
|
+
data: SerializedSelection,
|
|
24
|
+
): Map<string, Set<string>> => {
|
|
25
|
+
const map = new Map<string, Set<string>>();
|
|
26
|
+
Object.entries(data).forEach(([folderId, ids]) => {
|
|
27
|
+
map.set(folderId, new Set(ids));
|
|
28
|
+
});
|
|
29
|
+
return map;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const loadFromStorage = (
|
|
33
|
+
storageKey: string,
|
|
34
|
+
): Map<string, Set<string>> | undefined => {
|
|
35
|
+
try {
|
|
36
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
37
|
+
if (stored) {
|
|
38
|
+
const parsed = JSON.parse(stored) as SerializedSelection;
|
|
39
|
+
return deserializeSelection(parsed);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore storage errors
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const saveToStorage = (
|
|
48
|
+
selection: Map<string, Set<string>>,
|
|
49
|
+
storageKey: string,
|
|
50
|
+
) => {
|
|
51
|
+
try {
|
|
52
|
+
const serialized = serializeSelection(selection);
|
|
53
|
+
sessionStorage.setItem(storageKey, JSON.stringify(serialized));
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore storage errors
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const buildSelectionMap = (
|
|
60
|
+
folderIds: string[],
|
|
61
|
+
initialSelection?: Map<string, Set<string>>,
|
|
62
|
+
) => {
|
|
63
|
+
const map = new Map<string, Set<string>>();
|
|
64
|
+
folderIds.forEach((folderId) => {
|
|
65
|
+
const initial = initialSelection?.get(folderId);
|
|
66
|
+
map.set(folderId, initial ? new Set(initial) : new Set<string>());
|
|
67
|
+
});
|
|
68
|
+
return map;
|
|
69
|
+
};
|
|
11
70
|
|
|
12
71
|
export function createSelectionStore(
|
|
13
|
-
|
|
14
|
-
|
|
72
|
+
folderIds: string[],
|
|
73
|
+
storageKey: string = DEFAULT_STORAGE_KEY,
|
|
74
|
+
initialSelection?: Map<string, Set<string>>,
|
|
15
75
|
) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const storeRowById = get().rowById;
|
|
29
|
-
return new Set([...all].filter((id) => storeRowById.has(id)));
|
|
30
|
-
},
|
|
31
|
-
// Returns a Map of track IDs to RowInfo (no auto-generated IDs)
|
|
32
|
-
getTrackMap: () => {
|
|
33
|
-
const all = get().selectedIds;
|
|
34
|
-
const storeRowById = get().rowById;
|
|
35
|
-
const map = new Map<string, RowInfo>();
|
|
36
|
-
all.forEach((id) => {
|
|
37
|
-
if (storeRowById.has(id)) {
|
|
38
|
-
const row = storeRowById.get(id);
|
|
39
|
-
if (row) {
|
|
40
|
-
map.set(id, row);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
return map;
|
|
45
|
-
},
|
|
46
|
-
setSelected: (ids: Set<string>) =>
|
|
47
|
-
set(() => ({
|
|
48
|
-
selectedIds: new Set(ids),
|
|
49
|
-
})),
|
|
50
|
-
removeIds: (removedIds: Set<string>) =>
|
|
76
|
+
const storedSelection = loadFromStorage(storageKey);
|
|
77
|
+
// Storage wins: use stored if exists, else fall back to initialSelection
|
|
78
|
+
const selectedByFolder = buildSelectionMap(
|
|
79
|
+
folderIds,
|
|
80
|
+
storedSelection ?? initialSelection,
|
|
81
|
+
);
|
|
82
|
+
const activeFolderId = folderIds[0] ?? "";
|
|
83
|
+
|
|
84
|
+
const store = create<SelectionState & SelectionAction>((set) => ({
|
|
85
|
+
selectedByFolder,
|
|
86
|
+
activeFolderId,
|
|
87
|
+
clear: (folderId?: string) =>
|
|
51
88
|
set((state) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
next.
|
|
89
|
+
if (folderId) {
|
|
90
|
+
const next = new Map(state.selectedByFolder);
|
|
91
|
+
next.set(folderId, new Set<string>());
|
|
92
|
+
return { selectedByFolder: next };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const next = new Map<string, Set<string>>();
|
|
96
|
+
state.selectedByFolder.forEach((_value, id) => {
|
|
97
|
+
next.set(id, new Set<string>());
|
|
55
98
|
});
|
|
56
|
-
return {
|
|
99
|
+
return { selectedByFolder: next };
|
|
100
|
+
}),
|
|
101
|
+
setActiveFolder: (folderId: string) =>
|
|
102
|
+
set(() => ({ activeFolderId: folderId })),
|
|
103
|
+
setSelection: (folderId: string, ids: Set<string>) =>
|
|
104
|
+
set((state) => {
|
|
105
|
+
const next = new Map(state.selectedByFolder);
|
|
106
|
+
next.set(folderId, new Set(ids));
|
|
107
|
+
return { selectedByFolder: next };
|
|
57
108
|
}),
|
|
58
|
-
clear: () => set(() => ({ selectedIds: new Set<string>() })),
|
|
59
109
|
}));
|
|
110
|
+
|
|
111
|
+
// Subscribe to changes and persist to storage
|
|
112
|
+
store.subscribe((state) => {
|
|
113
|
+
saveToStorage(state.selectedByFolder, storageKey);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return store;
|
|
60
117
|
}
|
package/src/TrackSelect/types.ts
CHANGED
|
@@ -1,57 +1,12 @@
|
|
|
1
|
-
import { FuseOptionKey } from "fuse.js";
|
|
2
1
|
import { UseTreeItemParameters } from "@mui/x-tree-view/useTreeItem";
|
|
3
2
|
import { TreeViewBaseItem } from "@mui/x-tree-view";
|
|
4
3
|
import {
|
|
5
4
|
DataGridPremiumProps,
|
|
6
|
-
|
|
5
|
+
GridColDef,
|
|
6
|
+
GridRenderCellParams,
|
|
7
7
|
} from "@mui/x-data-grid-premium";
|
|
8
8
|
import { ReactElement, ReactNode } from "react";
|
|
9
9
|
import { SvgIconOwnProps } from "@mui/material";
|
|
10
|
-
import { SelectionStoreInstance } from "./store";
|
|
11
|
-
|
|
12
|
-
export interface SearchTracksProps {
|
|
13
|
-
query: string;
|
|
14
|
-
keyWeightMap: FuseOptionKey<any>[];
|
|
15
|
-
jsonStructure?: string;
|
|
16
|
-
treeItems?: TreeViewBaseItem<ExtendedTreeItemProps>[];
|
|
17
|
-
threshold?: number;
|
|
18
|
-
limit?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Types for the JSON-formatted tracks fomr modifiedHumanTracks.json
|
|
23
|
-
*/
|
|
24
|
-
export type AssayInfo = {
|
|
25
|
-
id: string;
|
|
26
|
-
assay: string;
|
|
27
|
-
url: string;
|
|
28
|
-
experimentAccession: string;
|
|
29
|
-
fileAccession: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type TrackInfo = {
|
|
33
|
-
name: string;
|
|
34
|
-
ontology: string;
|
|
35
|
-
lifeStage: string;
|
|
36
|
-
sampleType: string;
|
|
37
|
-
displayname: string;
|
|
38
|
-
assays: AssayInfo[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Row format for DataGrid
|
|
43
|
-
*/
|
|
44
|
-
export type RowInfo = {
|
|
45
|
-
id: string;
|
|
46
|
-
ontology: string;
|
|
47
|
-
lifeStage: string;
|
|
48
|
-
sampleType: string;
|
|
49
|
-
displayname: string;
|
|
50
|
-
assay: string;
|
|
51
|
-
experimentAccession: string;
|
|
52
|
-
fileAccession: string;
|
|
53
|
-
url: string;
|
|
54
|
-
};
|
|
55
10
|
|
|
56
11
|
/**
|
|
57
12
|
* Custom Tree Props for RichTreeView Panel
|
|
@@ -60,6 +15,7 @@ export type ExtendedTreeItemProps = {
|
|
|
60
15
|
id: string;
|
|
61
16
|
label: string;
|
|
62
17
|
icon: string;
|
|
18
|
+
folderId?: string;
|
|
63
19
|
isAssayItem?: boolean;
|
|
64
20
|
/**
|
|
65
21
|
* The assay name for leaf nodes (experiment accession items)
|
|
@@ -70,15 +26,16 @@ export type ExtendedTreeItemProps = {
|
|
|
70
26
|
* this is used in updating the rowSelectionModel when removing items from the Tree View panel
|
|
71
27
|
*/
|
|
72
28
|
allExpAccessions?: string[];
|
|
73
|
-
// list to allow search functionality in the treeview
|
|
74
|
-
allRowInfo?: RowInfo[];
|
|
75
29
|
};
|
|
76
30
|
|
|
77
31
|
export type TreeViewWrapperProps = {
|
|
78
|
-
store: SelectionStoreInstance;
|
|
79
32
|
items: TreeViewBaseItem<ExtendedTreeItemProps>[];
|
|
80
|
-
|
|
81
|
-
|
|
33
|
+
selectedCount: number;
|
|
34
|
+
onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
|
|
35
|
+
/** Optional custom TreeItem component */
|
|
36
|
+
TreeItemComponent?: React.ForwardRefExoticComponent<
|
|
37
|
+
CustomTreeItemProps & React.RefAttributes<HTMLLIElement>
|
|
38
|
+
>;
|
|
82
39
|
};
|
|
83
40
|
|
|
84
41
|
export interface CustomLabelProps {
|
|
@@ -86,38 +43,31 @@ export interface CustomLabelProps {
|
|
|
86
43
|
children: React.ReactNode;
|
|
87
44
|
isAssayItem?: boolean;
|
|
88
45
|
assayName?: string;
|
|
89
|
-
icon
|
|
46
|
+
icon?: React.ElementType | React.ReactElement | ReactNode;
|
|
47
|
+
/** Optional function to render custom icons for assay items */
|
|
48
|
+
renderIcon?: (name: string) => ReactNode;
|
|
90
49
|
}
|
|
91
50
|
|
|
92
51
|
export interface CustomTreeItemProps
|
|
93
52
|
extends Omit<UseTreeItemParameters, "rootRef">,
|
|
94
53
|
Omit<React.HTMLAttributes<HTMLLIElement>, "onFocus"> {
|
|
95
54
|
onRemove?: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
|
|
55
|
+
/** Optional function to render custom icons for assay items */
|
|
56
|
+
renderIcon?: (name: string) => ReactNode;
|
|
96
57
|
}
|
|
97
58
|
|
|
98
59
|
/**
|
|
99
60
|
* Types for useSelectionStore to keep track of selected DataGrid rows/tracks
|
|
100
61
|
*/
|
|
101
62
|
export type SelectionState = {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
assembly: string;
|
|
105
|
-
// All available rows for the current assembly
|
|
106
|
-
rows: RowInfo[];
|
|
107
|
-
// Map of id -> RowInfo for fast lookup
|
|
108
|
-
rowById: Map<string, RowInfo>;
|
|
109
|
-
// All selected IDs including auto-generated group IDs from DataGrid
|
|
110
|
-
selectedIds: Set<string>;
|
|
63
|
+
selectedByFolder: Map<string, Set<string>>;
|
|
64
|
+
activeFolderId: string;
|
|
111
65
|
};
|
|
112
66
|
|
|
113
67
|
export type SelectionAction = {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
getTrackMap: () => Map<string, RowInfo>;
|
|
118
|
-
setSelected: (ids: Set<string>) => void;
|
|
119
|
-
removeIds: (removedIds: Set<string>) => void;
|
|
120
|
-
clear: () => void;
|
|
68
|
+
clear: (folderId?: string) => void;
|
|
69
|
+
setActiveFolder: (folderId: string) => void;
|
|
70
|
+
setSelection: (folderId: string, ids: Set<string>) => void;
|
|
121
71
|
};
|
|
122
72
|
|
|
123
73
|
/**
|
|
@@ -140,11 +90,15 @@ interface BaseTableProps extends Omit<DataGridPremiumProps, "columns"> {
|
|
|
140
90
|
toolbarIconColor?: SvgIconOwnProps["htmlColor"];
|
|
141
91
|
}
|
|
142
92
|
|
|
143
|
-
type DataGridWrapperProps = {
|
|
144
|
-
rows:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
93
|
+
export type DataGridWrapperProps = {
|
|
94
|
+
rows: any[];
|
|
95
|
+
columns: GridColDef[];
|
|
96
|
+
groupingModel: string[];
|
|
97
|
+
leafField: string;
|
|
98
|
+
selectedIds: Set<string>;
|
|
99
|
+
onSelectionChange: (ids: Set<string>) => void;
|
|
100
|
+
/** Optional custom component for rendering grouping cells */
|
|
101
|
+
GroupingCellComponent?: React.FC<GridRenderCellParams>;
|
|
148
102
|
};
|
|
149
103
|
|
|
150
104
|
//This enforces that a downloadFileName is specified if a ReactElement is used as the label (no default )
|
package/src/lib.ts
CHANGED
|
@@ -7,5 +7,7 @@ import {
|
|
|
7
7
|
} from "./TrackSelect/store.ts";
|
|
8
8
|
export { createSelectionStore, SelectionStoreInstance };
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
export {
|
|
10
|
+
import { foldersByAssembly } from "./TrackSelect/Folders/index.ts";
|
|
11
|
+
export { foldersByAssembly };
|
|
12
|
+
|
|
13
|
+
export type { BiosampleRowInfo, GeneRowInfo } from "./TrackSelect/Folders";
|