@weng-lab/genomebrowser-ui 0.1.5 → 0.1.7
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/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
- package/dist/TrackSelect/store.d.ts +2 -2
- package/dist/TrackSelect/treeViewHelpers.d.ts +1 -0
- package/dist/TrackSelect/types.d.ts +10 -6
- package/dist/genomebrowser-ui.es.js +926 -1031
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/package.json +2 -2
- package/src/TrackSelect/.claude/settings.local.json +7 -0
- package/src/TrackSelect/DataGrid/CustomToolbar.tsx +0 -8
- package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +53 -58
- package/src/TrackSelect/DataGrid/columns.tsx +107 -97
- package/src/TrackSelect/TrackSelect.tsx +151 -104
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +6 -4
- package/src/TrackSelect/TreeView/treeViewHelpers.tsx +12 -13
- package/src/TrackSelect/store.ts +17 -9
- package/src/TrackSelect/treeViewHelpers.tsx +0 -0
- package/src/TrackSelect/types.ts +12 -6
- package/test/main.tsx +5 -8
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Box,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogActions,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogContentText,
|
|
8
|
+
DialogTitle,
|
|
5
9
|
FormControlLabel,
|
|
10
|
+
Stack,
|
|
6
11
|
Switch,
|
|
7
|
-
|
|
12
|
+
TextField,
|
|
8
13
|
} from "@mui/material";
|
|
14
|
+
import { GridRowSelectionModel } from "@mui/x-data-grid";
|
|
15
|
+
import { TreeViewBaseItem } from "@mui/x-tree-view";
|
|
16
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
9
17
|
import { DataGridWrapper } from "./DataGrid/DataGridWrapper";
|
|
10
|
-
import {
|
|
18
|
+
import { flattenIntoRow, searchTracks } from "./DataGrid/dataGridHelpers";
|
|
11
19
|
import { TreeViewWrapper } from "./TreeView/TreeViewWrapper";
|
|
12
20
|
import {
|
|
13
21
|
buildSortedAssayTreeView,
|
|
14
22
|
buildTreeView,
|
|
15
23
|
searchTreeItems,
|
|
16
24
|
} from "./TreeView/treeViewHelpers";
|
|
17
|
-
import {
|
|
18
|
-
import { rows, rowById } from "./consts";
|
|
19
|
-
import React, { useState, useMemo, useEffect } from "react";
|
|
20
|
-
import { TreeViewBaseItem } from "@mui/x-tree-view";
|
|
21
|
-
import { GridRowSelectionModel } from "@mui/x-data-grid";
|
|
22
|
-
import {
|
|
23
|
-
Dialog,
|
|
24
|
-
DialogTitle,
|
|
25
|
-
DialogContent,
|
|
26
|
-
DialogContentText,
|
|
27
|
-
DialogActions,
|
|
28
|
-
} from "@mui/material";
|
|
25
|
+
import { rowById, rows } from "./consts";
|
|
29
26
|
import { SelectionStoreInstance } from "./store";
|
|
27
|
+
import { ExtendedTreeItemProps, SearchTracksProps } from "./types";
|
|
30
28
|
|
|
31
29
|
export interface TrackSelectProps {
|
|
32
30
|
store: SelectionStoreInstance;
|
|
@@ -37,21 +35,19 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
37
35
|
const [sortedAssay, setSortedAssay] = useState(false);
|
|
38
36
|
const [searchQuery, setSearchQuery] = useState("");
|
|
39
37
|
const [isSearchResult, setIsSearchResult] = useState(false);
|
|
40
|
-
const
|
|
38
|
+
const selectedIds = store((s) => s.selectedIds);
|
|
39
|
+
const getTrackIds = store((s) => s.getTrackIds);
|
|
41
40
|
const setSelected = store((s) => s.setSelected);
|
|
42
41
|
const clear = store((s) => s.clear);
|
|
43
42
|
const MAX_ACTIVE = store((s) => s.maxTracks);
|
|
44
43
|
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
() => new Set(selectedTracks.keys()),
|
|
48
|
-
[selectedTracks],
|
|
49
|
-
);
|
|
44
|
+
// Get only real track IDs (no auto-generated group IDs)
|
|
45
|
+
const trackIds = useMemo(() => getTrackIds(), [selectedIds, getTrackIds]);
|
|
50
46
|
|
|
51
47
|
const treeItems = useMemo(() => {
|
|
52
48
|
return sortedAssay
|
|
53
49
|
? buildSortedAssayTreeView(
|
|
54
|
-
Array.from(
|
|
50
|
+
Array.from(trackIds),
|
|
55
51
|
{
|
|
56
52
|
id: "1",
|
|
57
53
|
isAssayItem: false,
|
|
@@ -63,7 +59,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
63
59
|
rowById,
|
|
64
60
|
)
|
|
65
61
|
: buildTreeView(
|
|
66
|
-
Array.from(
|
|
62
|
+
Array.from(trackIds),
|
|
67
63
|
{
|
|
68
64
|
id: "1",
|
|
69
65
|
isAssayItem: false,
|
|
@@ -74,7 +70,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
74
70
|
},
|
|
75
71
|
rowById,
|
|
76
72
|
);
|
|
77
|
-
}, [
|
|
73
|
+
}, [trackIds, sortedAssay]);
|
|
78
74
|
|
|
79
75
|
const [filteredRows, setFilteredRows] = useState(rows);
|
|
80
76
|
const [filteredTreeItems, setFilteredTreeItems] = useState([
|
|
@@ -87,116 +83,168 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
87
83
|
allRowInfo: [],
|
|
88
84
|
},
|
|
89
85
|
] as TreeViewBaseItem<ExtendedTreeItemProps>[]);
|
|
86
|
+
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
87
|
+
const searchResultIdsRef = useRef<Set<string>>(new Set());
|
|
90
88
|
|
|
91
89
|
useEffect(() => {
|
|
92
90
|
if (searchQuery === "") {
|
|
93
91
|
setFilteredTreeItems(treeItems);
|
|
94
92
|
setFilteredRows(rows);
|
|
95
93
|
setIsSearchResult(false);
|
|
94
|
+
searchResultIdsRef.current = new Set();
|
|
95
|
+
} else if (searchResultIdsRef.current.size > 0) {
|
|
96
|
+
// When selection changes during search, rebuild tree from selected items that match search
|
|
97
|
+
const matchingTrackIds = Array.from(trackIds).filter((id) =>
|
|
98
|
+
searchResultIdsRef.current.has(id),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const newTreeItems = sortedAssay
|
|
102
|
+
? buildSortedAssayTreeView(
|
|
103
|
+
matchingTrackIds,
|
|
104
|
+
{
|
|
105
|
+
id: "1",
|
|
106
|
+
isAssayItem: false,
|
|
107
|
+
label: "Biosamples",
|
|
108
|
+
icon: "folder",
|
|
109
|
+
children: [],
|
|
110
|
+
allRowInfo: [],
|
|
111
|
+
},
|
|
112
|
+
rowById,
|
|
113
|
+
)
|
|
114
|
+
: buildTreeView(
|
|
115
|
+
matchingTrackIds,
|
|
116
|
+
{
|
|
117
|
+
id: "1",
|
|
118
|
+
isAssayItem: false,
|
|
119
|
+
label: "Biosamples",
|
|
120
|
+
icon: "folder",
|
|
121
|
+
children: [],
|
|
122
|
+
allRowInfo: [],
|
|
123
|
+
},
|
|
124
|
+
rowById,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
setFilteredTreeItems(newTreeItems);
|
|
96
128
|
}
|
|
97
|
-
}, [treeItems, searchQuery]);
|
|
129
|
+
}, [treeItems, searchQuery, trackIds, sortedAssay]);
|
|
98
130
|
|
|
99
131
|
const handleToggle = () => {
|
|
100
132
|
setSortedAssay(!sortedAssay);
|
|
101
133
|
};
|
|
102
134
|
|
|
103
135
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
104
|
-
|
|
136
|
+
const query = e.target.value;
|
|
137
|
+
setSearchQuery(query);
|
|
138
|
+
|
|
139
|
+
// Clear previous timeout
|
|
140
|
+
if (searchTimeoutRef.current) {
|
|
141
|
+
clearTimeout(searchTimeoutRef.current);
|
|
142
|
+
}
|
|
105
143
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
query
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"ontology",
|
|
112
|
-
"lifeStage",
|
|
113
|
-
"sampleType",
|
|
114
|
-
"type",
|
|
115
|
-
"experimentAccession",
|
|
116
|
-
"fileAccession",
|
|
117
|
-
],
|
|
118
|
-
};
|
|
144
|
+
// Debounce the search
|
|
145
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
146
|
+
if (query === "") {
|
|
147
|
+
return; // useEffect handles empty query
|
|
148
|
+
}
|
|
119
149
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const newDataGridRows = searchTracks(dataGridSearchProps)
|
|
134
|
-
.map((t) => t.item)
|
|
135
|
-
.map(flattenIntoRow);
|
|
150
|
+
const dataGridSearchProps: SearchTracksProps = {
|
|
151
|
+
jsonStructure: "tracks",
|
|
152
|
+
query: query,
|
|
153
|
+
keyWeightMap: [
|
|
154
|
+
"displayname",
|
|
155
|
+
"ontology",
|
|
156
|
+
"lifeStage",
|
|
157
|
+
"sampleType",
|
|
158
|
+
"type",
|
|
159
|
+
"experimentAccession",
|
|
160
|
+
"fileAccession",
|
|
161
|
+
],
|
|
162
|
+
};
|
|
136
163
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
const treeSearchProps: SearchTracksProps = {
|
|
165
|
+
treeItems: treeItems,
|
|
166
|
+
query: query,
|
|
167
|
+
keyWeightMap: [
|
|
168
|
+
"displayname",
|
|
169
|
+
"ontology",
|
|
170
|
+
"lifeStage",
|
|
171
|
+
"sampleType",
|
|
172
|
+
"type",
|
|
173
|
+
"experimentAccession",
|
|
174
|
+
"fileAccession",
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
const newDataGridRows = searchTracks(dataGridSearchProps)
|
|
178
|
+
.map((t) => t.item)
|
|
179
|
+
.map(flattenIntoRow);
|
|
143
180
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
// we only want the intersection of filtered tracks displayed on the DataGrid and user-selected tracks to be displayed on the tree
|
|
182
|
+
const newDataGridIds = newDataGridRows.map((r) => r.experimentAccession);
|
|
183
|
+
const retIds = searchTreeItems(treeSearchProps).map(
|
|
184
|
+
(r) => r.item.experimentAccession,
|
|
185
|
+
);
|
|
186
|
+
const newTreeIds = retIds.filter((i) => newDataGridIds.includes(i));
|
|
187
|
+
|
|
188
|
+
// build new tree from the newTreeIds
|
|
189
|
+
const newTreeItems = sortedAssay
|
|
190
|
+
? buildSortedAssayTreeView(
|
|
191
|
+
newTreeIds,
|
|
192
|
+
{
|
|
193
|
+
id: "1",
|
|
194
|
+
isAssayItem: false,
|
|
195
|
+
label: "Biosamples",
|
|
196
|
+
icon: "folder",
|
|
197
|
+
children: [],
|
|
198
|
+
allRowInfo: [],
|
|
199
|
+
},
|
|
200
|
+
rowById,
|
|
201
|
+
)
|
|
202
|
+
: buildTreeView(
|
|
203
|
+
newTreeIds,
|
|
204
|
+
{
|
|
205
|
+
id: "1",
|
|
206
|
+
isAssayItem: false,
|
|
207
|
+
label: "Biosamples",
|
|
208
|
+
icon: "folder",
|
|
209
|
+
children: [],
|
|
210
|
+
allRowInfo: [],
|
|
211
|
+
},
|
|
212
|
+
rowById,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Store search result IDs in ref for use in useEffect
|
|
216
|
+
searchResultIdsRef.current = new Set(newDataGridIds);
|
|
170
217
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
218
|
+
setFilteredRows(newDataGridRows);
|
|
219
|
+
setIsSearchResult(true);
|
|
220
|
+
setFilteredTreeItems(newTreeItems);
|
|
221
|
+
}, 300);
|
|
174
222
|
};
|
|
175
223
|
|
|
176
224
|
const handleSelection = (newSelection: GridRowSelectionModel) => {
|
|
177
|
-
const
|
|
225
|
+
const allIds: Set<string> =
|
|
178
226
|
(newSelection && (newSelection as any).ids) ?? new Set<string>();
|
|
179
227
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
newTracks.set(id, row);
|
|
228
|
+
// Count only real track IDs for the limit check
|
|
229
|
+
let realTrackCount = 0;
|
|
230
|
+
allIds.forEach((id: string) => {
|
|
231
|
+
if (rowById.has(id)) {
|
|
232
|
+
realTrackCount++;
|
|
186
233
|
}
|
|
187
234
|
});
|
|
188
235
|
|
|
189
236
|
// Block only if the new selection would exceed the limit
|
|
190
|
-
if (
|
|
237
|
+
if (realTrackCount > MAX_ACTIVE) {
|
|
191
238
|
setLimitDialogOpen(true);
|
|
192
239
|
return;
|
|
193
240
|
}
|
|
194
241
|
|
|
195
|
-
|
|
242
|
+
// Store ALL IDs (including auto-generated group IDs)
|
|
243
|
+
setSelected(allIds);
|
|
196
244
|
};
|
|
197
245
|
|
|
198
246
|
return (
|
|
199
|
-
<Box sx={{ flex: 1 }}>
|
|
247
|
+
<Box sx={{ flex: 1, pt: 1 }}>
|
|
200
248
|
<Box display="flex" justifyContent="space-between" sx={{ mb: 3 }}>
|
|
201
249
|
<TextField
|
|
202
250
|
id="outlined-suffix-shrink"
|
|
@@ -222,7 +270,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
222
270
|
? `${filteredRows.length} Search Results`
|
|
223
271
|
: `${rows.length} Available Tracks`
|
|
224
272
|
}
|
|
225
|
-
|
|
273
|
+
selectedIds={selectedIds}
|
|
226
274
|
handleSelection={handleSelection}
|
|
227
275
|
sortedAssay={sortedAssay}
|
|
228
276
|
/>
|
|
@@ -231,8 +279,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
231
279
|
<TreeViewWrapper
|
|
232
280
|
store={store}
|
|
233
281
|
items={filteredTreeItems}
|
|
234
|
-
|
|
235
|
-
activeTracks={activeTracks}
|
|
282
|
+
trackIds={trackIds}
|
|
236
283
|
isSearchResult={isSearchResult}
|
|
237
284
|
/>
|
|
238
285
|
</Box>
|
|
@@ -12,7 +12,7 @@ import { Avatar } from "@mui/material";
|
|
|
12
12
|
export function TreeViewWrapper({
|
|
13
13
|
store,
|
|
14
14
|
items,
|
|
15
|
-
|
|
15
|
+
trackIds,
|
|
16
16
|
isSearchResult,
|
|
17
17
|
}: TreeViewWrapperProps) {
|
|
18
18
|
const removeIds = store((s) => s.removeIds);
|
|
@@ -28,11 +28,13 @@ export function TreeViewWrapper({
|
|
|
28
28
|
removedIds.forEach((id) => {
|
|
29
29
|
const row = rowById.get(id);
|
|
30
30
|
if (row) {
|
|
31
|
-
// Add the auto-generated group IDs for this track's
|
|
31
|
+
// Add the auto-generated group IDs for this track's grouping hierarchy
|
|
32
|
+
// Default view: ontology -> displayname
|
|
32
33
|
idsToRemove.add(`auto-generated-row-ontology/${row.ontology}`);
|
|
33
34
|
idsToRemove.add(
|
|
34
|
-
`auto-generated-row-ontology/${row.ontology}-
|
|
35
|
+
`auto-generated-row-ontology/${row.ontology}-displayname/${row.displayname}`,
|
|
35
36
|
);
|
|
37
|
+
// Sorted by assay view: assay -> ontology -> displayname
|
|
36
38
|
idsToRemove.add(`auto-generated-row-assay/${row.assay}`);
|
|
37
39
|
idsToRemove.add(
|
|
38
40
|
`auto-generated-row-assay/${row.assay}-ontology/${row.ontology}`,
|
|
@@ -76,7 +78,7 @@ export function TreeViewWrapper({
|
|
|
76
78
|
color: "text.primary",
|
|
77
79
|
}}
|
|
78
80
|
>
|
|
79
|
-
{
|
|
81
|
+
{trackIds.size}
|
|
80
82
|
</Avatar>
|
|
81
83
|
<Typography fontWeight="bold">
|
|
82
84
|
Active Tracks
|
|
@@ -99,8 +99,10 @@ export function buildSortedAssayTreeView(
|
|
|
99
99
|
id: row.experimentAccession,
|
|
100
100
|
isAssayItem: false,
|
|
101
101
|
label: row.experimentAccession,
|
|
102
|
-
icon:
|
|
102
|
+
icon: "removeable",
|
|
103
|
+
assayName: row.assay,
|
|
103
104
|
children: [],
|
|
105
|
+
allExpAccessions: [row.experimentAccession],
|
|
104
106
|
};
|
|
105
107
|
sampleAssayMap.set(row.displayname + row.assay, expNode);
|
|
106
108
|
displayNameNode.children!.push(expNode);
|
|
@@ -183,8 +185,10 @@ export function buildTreeView(
|
|
|
183
185
|
expNode = {
|
|
184
186
|
id: row.experimentAccession,
|
|
185
187
|
label: row.experimentAccession,
|
|
186
|
-
icon:
|
|
188
|
+
icon: "removeable",
|
|
189
|
+
assayName: row.assay,
|
|
187
190
|
children: [],
|
|
191
|
+
allExpAccessions: [row.experimentAccession],
|
|
188
192
|
};
|
|
189
193
|
sampleAssayMap.set(row.displayname + row.assay, expNode);
|
|
190
194
|
displayNameNode.children!.push(expNode);
|
|
@@ -282,7 +286,7 @@ const TreeItemLabelText = styled(Typography)({
|
|
|
282
286
|
fontFamily: "inherit",
|
|
283
287
|
});
|
|
284
288
|
|
|
285
|
-
function CustomLabel({ icon: Icon, children, isAssayItem, ...other }: CustomLabelProps) {
|
|
289
|
+
function CustomLabel({ icon: Icon, children, isAssayItem, assayName, ...other }: CustomLabelProps) {
|
|
286
290
|
const variant = isAssayItem ? "subtitle2" : "body2";
|
|
287
291
|
const fontWeight = isAssayItem ? "bold" : 500;
|
|
288
292
|
return (
|
|
@@ -305,8 +309,9 @@ function CustomLabel({ icon: Icon, children, isAssayItem, ...other }: CustomLabe
|
|
|
305
309
|
sx={{ mr: 1, fontSize: "1.2rem" }}
|
|
306
310
|
/>
|
|
307
311
|
)}
|
|
308
|
-
<Stack direction="row" spacing={
|
|
309
|
-
{
|
|
312
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
313
|
+
{isAssayItem && AssayIcon(other.id)}
|
|
314
|
+
{assayName && AssayIcon(assayName)}
|
|
310
315
|
<TreeItemLabelText fontWeight={fontWeight} variant={variant}>{children}</TreeItemLabelText>
|
|
311
316
|
</Stack>
|
|
312
317
|
</TreeItemLabel>
|
|
@@ -330,14 +335,7 @@ const TreeItemContent = styled("div")(({ theme }) => ({
|
|
|
330
335
|
marginBottom: theme.spacing(0.5),
|
|
331
336
|
marginTop: theme.spacing(0.5),
|
|
332
337
|
fontWeight: 500,
|
|
333
|
-
|
|
334
|
-
backgroundColor: theme.palette.primary.dark,
|
|
335
|
-
color: theme.palette.primary.contrastText,
|
|
336
|
-
...theme.applyStyles("light", {
|
|
337
|
-
backgroundColor: theme.palette.primary.main,
|
|
338
|
-
}),
|
|
339
|
-
},
|
|
340
|
-
"&:not([data-focused], [data-selected]):hover": {
|
|
338
|
+
"&:hover": {
|
|
341
339
|
backgroundColor: alpha(theme.palette.primary.main, 0.1),
|
|
342
340
|
color: "white",
|
|
343
341
|
...theme.applyStyles("light", {
|
|
@@ -417,6 +415,7 @@ export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
|
|
|
417
415
|
),
|
|
418
416
|
expandable: (status.expandable && status.expanded).toString(),
|
|
419
417
|
isAssayItem: item.isAssayItem,
|
|
418
|
+
assayName: item.assayName,
|
|
420
419
|
id: item.id
|
|
421
420
|
})}
|
|
422
421
|
/>
|
package/src/TrackSelect/store.ts
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
import { create, StoreApi, UseBoundStore } from "zustand";
|
|
2
|
-
import {
|
|
2
|
+
import { SelectionAction, SelectionState } from "./types";
|
|
3
3
|
|
|
4
4
|
export type SelectionStoreInstance = UseBoundStore<
|
|
5
5
|
StoreApi<SelectionState & SelectionAction>
|
|
6
6
|
>;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Helper to check if an ID is auto-generated by DataGrid grouping
|
|
9
|
+
const isAutoGeneratedId = (id: string) => id.startsWith("auto-generated-row-");
|
|
10
|
+
|
|
11
|
+
export function createSelectionStore(initialIds?: Set<string>) {
|
|
9
12
|
return create<SelectionState & SelectionAction>((set, get) => ({
|
|
10
13
|
maxTracks: 30,
|
|
11
|
-
|
|
12
|
-
selectedIds: ()
|
|
13
|
-
|
|
14
|
+
// Stores ALL selected IDs, including auto-generated group IDs
|
|
15
|
+
selectedIds: initialIds ? new Set(initialIds) : new Set<string>(),
|
|
16
|
+
// Returns only real track IDs (filters out auto-generated group IDs)
|
|
17
|
+
getTrackIds: () => {
|
|
18
|
+
const all = get().selectedIds;
|
|
19
|
+
return new Set([...all].filter((id) => !isAutoGeneratedId(id)));
|
|
20
|
+
},
|
|
21
|
+
setSelected: (ids: Set<string>) =>
|
|
14
22
|
set(() => ({
|
|
15
|
-
|
|
23
|
+
selectedIds: new Set(ids),
|
|
16
24
|
})),
|
|
17
25
|
removeIds: (removedIds: Set<string>) =>
|
|
18
26
|
set((state) => {
|
|
19
|
-
const next = new
|
|
27
|
+
const next = new Set(state.selectedIds);
|
|
20
28
|
removedIds.forEach((id) => {
|
|
21
29
|
next.delete(id);
|
|
22
30
|
});
|
|
23
|
-
return {
|
|
31
|
+
return { selectedIds: next };
|
|
24
32
|
}),
|
|
25
|
-
clear: () => set(() => ({
|
|
33
|
+
clear: () => set(() => ({ selectedIds: new Set<string>() })),
|
|
26
34
|
}));
|
|
27
35
|
}
|
|
File without changes
|
package/src/TrackSelect/types.ts
CHANGED
|
@@ -59,6 +59,10 @@ export type ExtendedTreeItemProps = {
|
|
|
59
59
|
label: string;
|
|
60
60
|
icon: string;
|
|
61
61
|
isAssayItem?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The assay name for leaf nodes (experiment accession items)
|
|
64
|
+
*/
|
|
65
|
+
assayName?: string;
|
|
62
66
|
/**
|
|
63
67
|
* list of all the experimentAccession values in the children/grandchildren of the item, or the accession of the item itself
|
|
64
68
|
* this is used in updating the rowSelectionModel when removing items from the Tree View panel
|
|
@@ -71,8 +75,7 @@ export type ExtendedTreeItemProps = {
|
|
|
71
75
|
export type TreeViewWrapperProps = {
|
|
72
76
|
store: SelectionStoreInstance;
|
|
73
77
|
items: TreeViewBaseItem<ExtendedTreeItemProps>[];
|
|
74
|
-
|
|
75
|
-
activeTracks: Set<string>; // doesn't have the autogenerated row groupings to provide accurate number of tracks
|
|
78
|
+
trackIds: Set<string>; // real track IDs only (no auto-generated)
|
|
76
79
|
isSearchResult: boolean;
|
|
77
80
|
};
|
|
78
81
|
|
|
@@ -80,6 +83,7 @@ export interface CustomLabelProps {
|
|
|
80
83
|
id: string;
|
|
81
84
|
children: React.ReactNode;
|
|
82
85
|
isAssayItem?: boolean;
|
|
86
|
+
assayName?: string;
|
|
83
87
|
icon: React.ElementType | React.ReactElement;
|
|
84
88
|
}
|
|
85
89
|
|
|
@@ -94,12 +98,14 @@ export interface CustomTreeItemProps
|
|
|
94
98
|
*/
|
|
95
99
|
export type SelectionState = {
|
|
96
100
|
maxTracks: number;
|
|
97
|
-
|
|
101
|
+
// All selected IDs including auto-generated group IDs from DataGrid
|
|
102
|
+
selectedIds: Set<string>;
|
|
98
103
|
};
|
|
99
104
|
|
|
100
105
|
export type SelectionAction = {
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
// Returns only real track IDs (filters out auto-generated group IDs)
|
|
107
|
+
getTrackIds: () => Set<string>;
|
|
108
|
+
setSelected: (ids: Set<string>) => void;
|
|
103
109
|
removeIds: (removedIds: Set<string>) => void;
|
|
104
110
|
clear: () => void;
|
|
105
111
|
};
|
|
@@ -126,7 +132,7 @@ interface BaseTableProps extends Omit<DataGridPremiumProps, "columns"> {
|
|
|
126
132
|
|
|
127
133
|
type DataGridWrapperProps = {
|
|
128
134
|
rows: RowInfo[];
|
|
129
|
-
|
|
135
|
+
selectedIds: Set<string>; // all IDs including auto-generated group IDs
|
|
130
136
|
handleSelection: (newSelection: GridRowSelectionModel) => void;
|
|
131
137
|
sortedAssay: boolean;
|
|
132
138
|
};
|
package/test/main.tsx
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createSelectionStore } from "../src/TrackSelect/store";
|
|
2
|
+
import TrackSelect from "../src/TrackSelect/TrackSelect";
|
|
2
3
|
import { createRoot } from "react-dom/client";
|
|
3
4
|
|
|
4
5
|
function Main() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</TrackSelect>
|
|
8
|
-
)
|
|
6
|
+
const store = createSelectionStore();
|
|
7
|
+
return <TrackSelect store={store} />;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
createRoot(document.getElementById("root")!).render(
|
|
12
|
-
<Main/>
|
|
13
|
-
);
|
|
10
|
+
createRoot(document.getElementById("root")!).render(<Main />);
|