@weng-lab/genomebrowser-ui 0.1.12 → 0.2.0-beta.0

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 (84) hide show
  1. package/.env.local +1 -0
  2. package/dist/TrackSelect/DataGrid/DefaultGroupingCell.d.ts +6 -0
  3. package/dist/TrackSelect/FolderList/Breadcrumb.d.ts +6 -0
  4. package/dist/TrackSelect/FolderList/FolderCard.d.ts +6 -0
  5. package/dist/TrackSelect/FolderList/FolderList.d.ts +6 -0
  6. package/dist/TrackSelect/{Data/humanBiosamples.json.d.ts → Folders/biosamples/data/human.json.d.ts} +1940 -1919
  7. package/dist/TrackSelect/{Data/mouseBiosamples.json.d.ts → Folders/biosamples/data/mouse.json.d.ts} +408 -357
  8. package/dist/TrackSelect/Folders/biosamples/human.d.ts +7 -0
  9. package/dist/TrackSelect/Folders/biosamples/mouse.d.ts +7 -0
  10. package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +14 -0
  11. package/dist/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.d.ts +6 -0
  12. package/dist/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.d.ts +7 -0
  13. package/dist/TrackSelect/Folders/biosamples/shared/columns.d.ts +14 -0
  14. package/dist/TrackSelect/Folders/biosamples/shared/constants.d.ts +19 -0
  15. package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +24 -0
  16. package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +25 -0
  17. package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +44 -0
  18. package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +10 -0
  19. package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +10 -0
  20. package/dist/TrackSelect/Folders/genes/human.d.ts +7 -0
  21. package/dist/TrackSelect/Folders/genes/mouse.d.ts +7 -0
  22. package/dist/TrackSelect/Folders/genes/shared/columns.d.ts +14 -0
  23. package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +12 -0
  24. package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +13 -0
  25. package/dist/TrackSelect/Folders/genes/shared/types.d.ts +26 -0
  26. package/dist/TrackSelect/Folders/index.d.ts +14 -0
  27. package/dist/TrackSelect/Folders/types.d.ts +76 -0
  28. package/dist/TrackSelect/TrackSelect.d.ts +12 -5
  29. package/dist/TrackSelect/TreeView/CustomTreeItem.d.ts +3 -0
  30. package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
  31. package/dist/TrackSelect/store.d.ts +1 -2
  32. package/dist/TrackSelect/types.d.ts +24 -62
  33. package/dist/genomebrowser-ui.es.js +1373 -2117
  34. package/dist/genomebrowser-ui.es.js.map +1 -1
  35. package/dist/lib.d.ts +2 -2
  36. package/package.json +3 -2
  37. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +36 -20
  38. package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -0
  39. package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -0
  40. package/src/TrackSelect/FolderList/FolderCard.tsx +51 -0
  41. package/src/TrackSelect/FolderList/FolderList.tsx +47 -0
  42. package/src/TrackSelect/Folders/NEW.md +929 -0
  43. package/src/TrackSelect/{Data → Folders/biosamples/data}/formatBiosamples.go +2 -2
  44. package/src/TrackSelect/{Data/humanBiosamples.json → Folders/biosamples/data/human.json} +1940 -1919
  45. package/src/TrackSelect/{Data/mouseBiosamples.json → Folders/biosamples/data/mouse.json} +408 -357
  46. package/src/TrackSelect/Folders/biosamples/human.ts +17 -0
  47. package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -0
  48. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +65 -0
  49. package/src/TrackSelect/{DataGrid/GroupingCell.tsx → Folders/biosamples/shared/BiosampleGroupingCell.tsx} +7 -5
  50. package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -0
  51. package/src/TrackSelect/{DataGrid → Folders/biosamples/shared}/columns.tsx +31 -17
  52. package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -0
  53. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -0
  54. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +227 -0
  55. package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -0
  56. package/src/TrackSelect/Folders/genes/data/human.json +7 -0
  57. package/src/TrackSelect/Folders/genes/data/mouse.json +7 -0
  58. package/src/TrackSelect/Folders/genes/human.ts +16 -0
  59. package/src/TrackSelect/Folders/genes/mouse.ts +16 -0
  60. package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -0
  61. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -0
  62. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -0
  63. package/src/TrackSelect/Folders/genes/shared/types.ts +29 -0
  64. package/src/TrackSelect/Folders/index.ts +27 -0
  65. package/src/TrackSelect/Folders/types.ts +95 -0
  66. package/src/TrackSelect/TrackSelect.tsx +409 -311
  67. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +217 -0
  68. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +47 -42
  69. package/src/TrackSelect/store.ts +103 -46
  70. package/src/TrackSelect/types.ts +28 -74
  71. package/src/lib.ts +2 -2
  72. package/test/main.tsx +112 -168
  73. package/.claude/settings.local.json +0 -7
  74. package/dist/TrackSelect/DataGrid/CustomToolbar.d.ts +0 -12
  75. package/dist/TrackSelect/DataGrid/GroupingCell.d.ts +0 -2
  76. package/dist/TrackSelect/DataGrid/columns.d.ts +0 -4
  77. package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +0 -49
  78. package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +0 -49
  79. package/dist/TrackSelect/consts.d.ts +0 -11
  80. package/src/TrackSelect/DataGrid/CustomToolbar.tsx +0 -152
  81. package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +0 -155
  82. package/src/TrackSelect/TreeView/treeViewHelpers.tsx +0 -475
  83. package/src/TrackSelect/consts.ts +0 -92
  84. 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 "./treeViewHelpers";
9
- import { Avatar } from "@mui/material";
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
- trackIds,
15
- isSearchResult,
29
+ selectedCount,
30
+ onRemove,
31
+ TreeItemComponent,
16
32
  }: TreeViewWrapperProps) {
17
- const removeIds = store((s) => s.removeIds);
18
- const rowById = store((s) => s.rowById);
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
- const removedIds = item.allExpAccessions;
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
- {trackIds.size}
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
- defaultExpandedItems={["1"]}
101
- slots={{ item: CustomTreeItem }}
104
+ expandedItems={expandedItems}
105
+ onExpandedItemsChange={(_event, ids) => setExpandedItems(ids)}
106
+ slots={{ item: TreeItem }}
102
107
  slotProps={{
103
108
  item: {
104
109
  onRemove: handleRemoveTreeItem,
@@ -1,60 +1,117 @@
1
1
  import { create, StoreApi, UseBoundStore } from "zustand";
2
- import { buildRowsForAssembly, Assembly } from "./consts";
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
- // Helper to check if an ID is auto-generated by DataGrid grouping
10
- // const isAutoGeneratedId = (id: string) => id.startsWith("auto-generated-row-");
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
- assembly: Assembly,
14
- initialIds?: Set<string>,
72
+ folderIds: string[],
73
+ storageKey: string = DEFAULT_STORAGE_KEY,
74
+ initialSelection?: Map<string, Set<string>>,
15
75
  ) {
16
- const { rows, rowById } = buildRowsForAssembly(assembly);
17
-
18
- return create<SelectionState & SelectionAction>((set, get) => ({
19
- maxTracks: 30,
20
- assembly,
21
- rows,
22
- rowById,
23
- // Stores ALL selected IDs, including auto-generated group IDs
24
- selectedIds: initialIds ? new Set(initialIds) : new Set<string>(),
25
- // Returns only real track IDs (filters out auto-generated group IDs)
26
- getTrackIds: () => {
27
- const all = get().selectedIds;
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
- const next = new Set(state.selectedIds);
53
- removedIds.forEach((id) => {
54
- next.delete(id);
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 { selectedIds: next };
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
  }
@@ -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
- GridRowSelectionModel,
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
- trackIds: Set<string>; // real track IDs only (no auto-generated)
81
- isSearchResult: boolean;
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: React.ElementType | React.ReactElement;
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
- maxTracks: number;
103
- // Assembly determines which JSON data to use
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
- // Returns only real track IDs (filters out auto-generated group IDs)
115
- getTrackIds: () => Set<string>;
116
- // Returns a Map of track IDs to RowInfo (no auto-generated IDs)
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: RowInfo[];
145
- selectedIds: Set<string>; // all IDs including auto-generated group IDs
146
- handleSelection: (newSelection: GridRowSelectionModel) => void;
147
- sortedAssay: boolean;
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,5 @@ import {
7
7
  } from "./TrackSelect/store.ts";
8
8
  export { createSelectionStore, SelectionStoreInstance };
9
9
 
10
- import type { RowInfo } from "./TrackSelect/types.ts";
11
- export { RowInfo };
10
+ import { foldersByAssembly } from "./TrackSelect/Folders/index.ts";
11
+ export { foldersByAssembly };