@weng-lab/genomebrowser-ui 0.1.9 → 0.1.10
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/Data/{modifiedHumanTracks.json.d.ts → humanBiosamples.json.d.ts} +40705 -20804
- package/dist/TrackSelect/Data/mouseBiosamples.json.d.ts +10346 -0
- package/dist/TrackSelect/DataGrid/GroupingCell.d.ts +2 -0
- package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +25 -6
- package/dist/TrackSelect/TrackSelect.d.ts +4 -1
- package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +1 -1
- package/dist/TrackSelect/consts.d.ts +6 -17
- package/dist/TrackSelect/store.d.ts +2 -1
- package/dist/TrackSelect/types.d.ts +5 -0
- package/dist/genomebrowser-ui.es.js +1173 -951
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/dist/lib.d.ts +0 -2
- package/package.json +2 -2
- package/src/TrackSelect/Data/formatBiosamples.go +254 -0
- package/src/TrackSelect/Data/{modifiedHumanTracks.json → humanBiosamples.json} +40704 -20804
- package/src/TrackSelect/Data/mouseBiosamples.json +10343 -0
- package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +13 -6
- package/src/TrackSelect/DataGrid/GroupingCell.tsx +144 -0
- package/src/TrackSelect/DataGrid/columns.tsx +7 -0
- package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +64 -19
- package/src/TrackSelect/TrackSelect.tsx +86 -27
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +1 -1
- package/src/TrackSelect/TreeView/treeViewHelpers.tsx +65 -17
- package/src/TrackSelect/consts.ts +30 -30
- package/src/TrackSelect/issues.md +404 -0
- package/src/TrackSelect/store.ts +16 -6
- package/src/TrackSelect/types.ts +8 -0
- package/src/lib.ts +0 -3
- package/test/main.tsx +399 -17
- package/dist/TrackSelect/treeViewHelpers.d.ts +0 -1
- package/src/TrackSelect/.claude/settings.local.json +0 -7
- package/src/TrackSelect/Data/humanTracks.json +0 -35711
- package/src/TrackSelect/Data/human_chromhmm_biosamples_with_all_urls.json +0 -35716
- package/src/TrackSelect/bug.md +0 -4
- package/src/TrackSelect/treeViewHelpers.tsx +0 -0
|
@@ -24,6 +24,44 @@ import Fuse, { FuseResult } from "fuse.js";
|
|
|
24
24
|
import { SearchTracksProps } from "../types";
|
|
25
25
|
import { assayTypes, ontologyTypes } from "../consts";
|
|
26
26
|
|
|
27
|
+
/** Format an ID like "h3k27ac-ENCFF922YMQ" to "H3K27ac - ENCFF922YMQ" */
|
|
28
|
+
function formatIdLabel(id: string): string {
|
|
29
|
+
const hyphenIndex = id.indexOf("-");
|
|
30
|
+
if (hyphenIndex === -1) return id;
|
|
31
|
+
|
|
32
|
+
const assayPart = id.substring(0, hyphenIndex);
|
|
33
|
+
let accessionPart = id.substring(hyphenIndex + 1);
|
|
34
|
+
|
|
35
|
+
// Truncate accession parts to 15 characters
|
|
36
|
+
if (accessionPart.length > 25)
|
|
37
|
+
accessionPart = accessionPart.substring(0, 15) + "…";
|
|
38
|
+
|
|
39
|
+
const formattedAssay = formatAssayName(assayPart);
|
|
40
|
+
return `${formattedAssay} - ${accessionPart}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatAssayName(assay: string): string {
|
|
44
|
+
switch (assay.toLowerCase()) {
|
|
45
|
+
case "dnase":
|
|
46
|
+
return "DNase";
|
|
47
|
+
case "atac":
|
|
48
|
+
return "ATAC";
|
|
49
|
+
case "h3k4me3":
|
|
50
|
+
return "H3K4me3";
|
|
51
|
+
case "h3k27ac":
|
|
52
|
+
return "H3K27ac";
|
|
53
|
+
case "ctcf":
|
|
54
|
+
return "CTCF";
|
|
55
|
+
case "chromhmm":
|
|
56
|
+
return "ChromHMM";
|
|
57
|
+
case "ccre":
|
|
58
|
+
return "cCRE";
|
|
59
|
+
case "rnaseq":
|
|
60
|
+
return "RNA-seq";
|
|
61
|
+
default:
|
|
62
|
+
return assay;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
27
65
|
|
|
28
66
|
/**
|
|
29
67
|
* Builds tree in the sorted by assay view
|
|
@@ -93,23 +131,23 @@ export function buildSortedAssayTreeView(
|
|
|
93
131
|
};
|
|
94
132
|
ontologyNode.children!.push(displayNameNode);
|
|
95
133
|
|
|
96
|
-
let expNode = sampleAssayMap.get(row.displayname + row.
|
|
134
|
+
let expNode = sampleAssayMap.get(row.displayname + row.id);
|
|
97
135
|
if (!expNode) {
|
|
98
136
|
expNode = {
|
|
99
|
-
id: row.
|
|
137
|
+
id: row.id,
|
|
100
138
|
isAssayItem: false,
|
|
101
|
-
label: row.
|
|
139
|
+
label: formatIdLabel(row.id),
|
|
102
140
|
icon: "removeable",
|
|
103
141
|
assayName: row.assay,
|
|
104
142
|
children: [],
|
|
105
|
-
allExpAccessions: [row.
|
|
143
|
+
allExpAccessions: [row.id],
|
|
106
144
|
};
|
|
107
145
|
sampleAssayMap.set(row.displayname + row.assay, expNode);
|
|
108
146
|
displayNameNode.children!.push(expNode);
|
|
109
147
|
}
|
|
110
|
-
assayNode.allExpAccessions!.push(row.
|
|
111
|
-
ontologyNode.allExpAccessions!.push(row.
|
|
112
|
-
displayNameNode.allExpAccessions!.push(row.
|
|
148
|
+
assayNode.allExpAccessions!.push(row.id);
|
|
149
|
+
ontologyNode.allExpAccessions!.push(row.id);
|
|
150
|
+
displayNameNode.allExpAccessions!.push(row.id);
|
|
113
151
|
root.allRowInfo!.push(row);
|
|
114
152
|
});
|
|
115
153
|
// standardize the order of the assay folders everytime one is added
|
|
@@ -183,18 +221,18 @@ export function buildTreeView(
|
|
|
183
221
|
let expNode = sampleAssayMap.get(row.displayname + row.assay);
|
|
184
222
|
if (!expNode) {
|
|
185
223
|
expNode = {
|
|
186
|
-
id: row.
|
|
187
|
-
label: row.
|
|
224
|
+
id: row.id,
|
|
225
|
+
label: formatIdLabel(row.id),
|
|
188
226
|
icon: "removeable",
|
|
189
227
|
assayName: row.assay,
|
|
190
228
|
children: [],
|
|
191
|
-
allExpAccessions: [row.
|
|
229
|
+
allExpAccessions: [row.id],
|
|
192
230
|
};
|
|
193
231
|
sampleAssayMap.set(row.displayname + row.assay, expNode);
|
|
194
232
|
displayNameNode.children!.push(expNode);
|
|
195
233
|
}
|
|
196
|
-
ontologyNode.allExpAccessions!.push(row.
|
|
197
|
-
displayNameNode.allExpAccessions!.push(row.
|
|
234
|
+
ontologyNode.allExpAccessions!.push(row.id);
|
|
235
|
+
displayNameNode.allExpAccessions!.push(row.id);
|
|
198
236
|
root.allRowInfo!.push(row);
|
|
199
237
|
});
|
|
200
238
|
// standardize the order of the assay folders everytime one is added
|
|
@@ -230,7 +268,7 @@ export function searchTreeItems({
|
|
|
230
268
|
query,
|
|
231
269
|
keyWeightMap,
|
|
232
270
|
threshold,
|
|
233
|
-
limit = 10
|
|
271
|
+
limit = 10,
|
|
234
272
|
}: SearchTracksProps): FuseResult<RowInfo>[] {
|
|
235
273
|
const data = treeItems![0].allRowInfo ?? [];
|
|
236
274
|
const fuse = new Fuse(data, {
|
|
@@ -255,6 +293,8 @@ export function AssayIcon(type: string) {
|
|
|
255
293
|
ChromHMM: "#0097a7",
|
|
256
294
|
H3K27ac: "#fdc401",
|
|
257
295
|
CTCF: "#01a6f1",
|
|
296
|
+
cCRE: "#8b5cf6",
|
|
297
|
+
"RNA-seq": "#f97316",
|
|
258
298
|
};
|
|
259
299
|
const color = colorMap[type];
|
|
260
300
|
return (
|
|
@@ -286,7 +326,13 @@ const TreeItemLabelText = styled(Typography)({
|
|
|
286
326
|
fontFamily: "inherit",
|
|
287
327
|
});
|
|
288
328
|
|
|
289
|
-
function CustomLabel({
|
|
329
|
+
function CustomLabel({
|
|
330
|
+
icon: Icon,
|
|
331
|
+
children,
|
|
332
|
+
isAssayItem,
|
|
333
|
+
assayName,
|
|
334
|
+
...other
|
|
335
|
+
}: CustomLabelProps) {
|
|
290
336
|
const variant = isAssayItem ? "subtitle2" : "body2";
|
|
291
337
|
const fontWeight = isAssayItem ? "bold" : 500;
|
|
292
338
|
return (
|
|
@@ -312,7 +358,9 @@ function CustomLabel({ icon: Icon, children, isAssayItem, assayName, ...other }:
|
|
|
312
358
|
<Stack direction="row" spacing={1} alignItems="center">
|
|
313
359
|
{isAssayItem && AssayIcon(other.id)}
|
|
314
360
|
{assayName && AssayIcon(assayName)}
|
|
315
|
-
<TreeItemLabelText fontWeight={fontWeight} variant={variant}>
|
|
361
|
+
<TreeItemLabelText fontWeight={fontWeight} variant={variant}>
|
|
362
|
+
{children}
|
|
363
|
+
</TreeItemLabelText>
|
|
316
364
|
</Stack>
|
|
317
365
|
</TreeItemLabel>
|
|
318
366
|
);
|
|
@@ -416,7 +464,7 @@ export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
|
|
|
416
464
|
expandable: (status.expandable && status.expanded).toString(),
|
|
417
465
|
isAssayItem: item.isAssayItem,
|
|
418
466
|
assayName: item.assayName,
|
|
419
|
-
id: item.id
|
|
467
|
+
id: item.id,
|
|
420
468
|
})}
|
|
421
469
|
/>
|
|
422
470
|
</TreeItemContent>
|
|
@@ -424,4 +472,4 @@ export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
|
|
|
424
472
|
</TreeItemRoot>
|
|
425
473
|
</TreeItemProvider>
|
|
426
474
|
);
|
|
427
|
-
});
|
|
475
|
+
});
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getTracksByAssayAndOntology,
|
|
3
|
-
|
|
3
|
+
flattenIntoRows,
|
|
4
|
+
getTracksData,
|
|
4
5
|
} from "./DataGrid/dataGridHelpers";
|
|
5
6
|
import { RowInfo, TrackInfo } from "./types";
|
|
6
7
|
|
|
8
|
+
export type Assembly = "GRCh38" | "mm10";
|
|
9
|
+
|
|
7
10
|
export const assayTypes = [
|
|
11
|
+
"cCRE",
|
|
8
12
|
"DNase",
|
|
9
13
|
"H3K4me3",
|
|
10
14
|
"H3K27ac",
|
|
11
15
|
"ATAC",
|
|
12
16
|
"CTCF",
|
|
17
|
+
"RNA-seq",
|
|
13
18
|
"ChromHMM",
|
|
14
19
|
];
|
|
15
20
|
|
|
@@ -59,34 +64,29 @@ export const ontologyTypes = [
|
|
|
59
64
|
"Vagina",
|
|
60
65
|
];
|
|
61
66
|
|
|
62
|
-
export const rows = ontologyTypes.flatMap((ontology) =>
|
|
63
|
-
assayTypes.flatMap((assay) =>
|
|
64
|
-
getTracksByAssayAndOntology(
|
|
65
|
-
assay.toLowerCase(),
|
|
66
|
-
ontology.toLowerCase(),
|
|
67
|
-
).map((r: TrackInfo) => {
|
|
68
|
-
const flat = flattenIntoRow(r);
|
|
69
|
-
return {
|
|
70
|
-
...flat,
|
|
71
|
-
assay,
|
|
72
|
-
ontology,
|
|
73
|
-
};
|
|
74
|
-
}),
|
|
75
|
-
),
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// map of experimentAccession -> rowInfo for faster row lookup
|
|
79
|
-
export const rowById = new Map<string, RowInfo>(
|
|
80
|
-
rows.map((r) => [r.experimentAccession, r]),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Check if an ID is a real track (exists in rowById) vs an auto-generated group ID
|
|
85
|
-
*/
|
|
86
|
-
export const isTrackId = (id: string): boolean => rowById.has(id);
|
|
87
|
-
|
|
88
67
|
/**
|
|
89
|
-
*
|
|
68
|
+
* Build rows and rowById for a specific assembly
|
|
90
69
|
*/
|
|
91
|
-
export
|
|
92
|
-
|
|
70
|
+
export function buildRowsForAssembly(assembly: Assembly): {
|
|
71
|
+
rows: RowInfo[];
|
|
72
|
+
rowById: Map<string, RowInfo>;
|
|
73
|
+
} {
|
|
74
|
+
const tracksData = getTracksData(assembly);
|
|
75
|
+
const rows = ontologyTypes.flatMap((ontology) =>
|
|
76
|
+
assayTypes.flatMap((assay) =>
|
|
77
|
+
getTracksByAssayAndOntology(
|
|
78
|
+
assay.toLowerCase(),
|
|
79
|
+
ontology.toLowerCase(),
|
|
80
|
+
tracksData,
|
|
81
|
+
).flatMap((r: TrackInfo) =>
|
|
82
|
+
flattenIntoRows(r).map((flat) => ({
|
|
83
|
+
...flat,
|
|
84
|
+
assay,
|
|
85
|
+
ontology,
|
|
86
|
+
})),
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
const rowById = new Map<string, RowInfo>(rows.map((r) => [r.id, r]));
|
|
91
|
+
return { rows, rowById };
|
|
92
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# TrackSelect Code Review Issues
|
|
2
|
+
|
|
3
|
+
## Critical Issues
|
|
4
|
+
|
|
5
|
+
### ~~1. Bug: Hardcoded `rowById` Import Breaks Multi-Assembly Support~~ ✅ FIXED
|
|
6
|
+
|
|
7
|
+
**File:** `TreeViewWrapper.tsx:3,28-42`
|
|
8
|
+
|
|
9
|
+
~~**Problem:** The `rowById` from `consts.ts` is always built with `"GRCh38"` (human assembly). When the store is created with `mm10` assembly, the TreeViewWrapper will fail to find rows because it's looking in the wrong map.~~
|
|
10
|
+
|
|
11
|
+
**Resolution:** Removed hardcoded import, now gets `rowById` from the store.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### 2. Bug: Memory Leak from Missing Timeout Cleanup
|
|
16
|
+
|
|
17
|
+
**File:** `TrackSelect.tsx:117,170-173`
|
|
18
|
+
|
|
19
|
+
**Problem:** No cleanup on unmount. If component unmounts while timeout is pending, it will try to update state on an unmounted component.
|
|
20
|
+
|
|
21
|
+
**Fix - Add useEffect after line 118:**
|
|
22
|
+
```typescript
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
return () => {
|
|
25
|
+
if (searchTimeoutRef.current) {
|
|
26
|
+
clearTimeout(searchTimeoutRef.current);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
### 3. Bug: Unsafe Type Assertion with `as any`
|
|
35
|
+
|
|
36
|
+
**File:** `TrackSelect.tsx:254-256`
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const allIds: Set<string> =
|
|
40
|
+
(newSelection && (newSelection as any).ids) ?? new Set<string>();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Problem:** Using `as any` bypasses TypeScript safety.
|
|
44
|
+
|
|
45
|
+
**Fix:**
|
|
46
|
+
```typescript
|
|
47
|
+
let allIds: Set<string>;
|
|
48
|
+
if (
|
|
49
|
+
newSelection &&
|
|
50
|
+
typeof newSelection === 'object' &&
|
|
51
|
+
'ids' in newSelection &&
|
|
52
|
+
newSelection.ids instanceof Set
|
|
53
|
+
) {
|
|
54
|
+
allIds = newSelection.ids as Set<string>;
|
|
55
|
+
} else {
|
|
56
|
+
allIds = new Set<string>();
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### 4. Bug: Inconsistent Map Keys in `buildSortedAssayTreeView`
|
|
63
|
+
|
|
64
|
+
**File:** `TreeView/treeViewHelpers.tsx:134,145`
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Line 134 (get):
|
|
68
|
+
let expNode = sampleAssayMap.get(row.displayname + row.id);
|
|
69
|
+
// Line 145 (set):
|
|
70
|
+
sampleAssayMap.set(row.displayname + row.assay, expNode);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Problem:** Different keys used for get vs set - lookup will always return `undefined`.
|
|
74
|
+
|
|
75
|
+
**Fix Line 134:**
|
|
76
|
+
```typescript
|
|
77
|
+
let expNode = sampleAssayMap.get(row.displayname + row.assay);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Code Duplication Issues
|
|
83
|
+
|
|
84
|
+
### 5. Duplicate Assay Formatting Functions
|
|
85
|
+
|
|
86
|
+
**Files:**
|
|
87
|
+
- `DataGrid/dataGridHelpers.tsx:17-38` - `formatAssayType`
|
|
88
|
+
- `TreeView/treeViewHelpers.tsx:43-64` - `formatAssayName`
|
|
89
|
+
|
|
90
|
+
Nearly identical switch statements.
|
|
91
|
+
|
|
92
|
+
**Fix - Create shared utility in `consts.ts`:**
|
|
93
|
+
```typescript
|
|
94
|
+
export const ASSAY_DISPLAY_MAP: Record<string, string> = {
|
|
95
|
+
dnase: "DNase",
|
|
96
|
+
atac: "ATAC",
|
|
97
|
+
h3k4me3: "H3K4me3",
|
|
98
|
+
h3k27ac: "H3K27ac",
|
|
99
|
+
ctcf: "CTCF",
|
|
100
|
+
chromhmm: "ChromHMM",
|
|
101
|
+
ccre: "cCRE",
|
|
102
|
+
rnaseq: "RNA-seq",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function formatAssayType(assay: string): string {
|
|
106
|
+
return ASSAY_DISPLAY_MAP[assay.toLowerCase()] ?? assay;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### 6. Duplicate Root Tree Item Object (6 occurrences)
|
|
113
|
+
|
|
114
|
+
**File:** `TrackSelect.tsx` - Lines 83, 95, 108, 134, 145, 222
|
|
115
|
+
|
|
116
|
+
Same object literal repeated 6 times:
|
|
117
|
+
```typescript
|
|
118
|
+
{
|
|
119
|
+
id: "1",
|
|
120
|
+
isAssayItem: false,
|
|
121
|
+
label: "Biosamples",
|
|
122
|
+
icon: "folder",
|
|
123
|
+
children: [],
|
|
124
|
+
allRowInfo: [],
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Fix - Add to `consts.ts`:**
|
|
129
|
+
```typescript
|
|
130
|
+
export const createRootTreeItem = (): TreeViewBaseItem<ExtendedTreeItemProps> => ({
|
|
131
|
+
id: "1",
|
|
132
|
+
isAssayItem: false,
|
|
133
|
+
label: "Biosamples",
|
|
134
|
+
icon: "folder",
|
|
135
|
+
children: [],
|
|
136
|
+
allRowInfo: [],
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 7. Duplicate Column renderCell Logic
|
|
143
|
+
|
|
144
|
+
**File:** `DataGrid/columns.tsx:19-92`
|
|
145
|
+
|
|
146
|
+
Similar renderCell implementations across multiple column definitions.
|
|
147
|
+
|
|
148
|
+
**Fix - Create helper functions:**
|
|
149
|
+
```typescript
|
|
150
|
+
const renderGroupCell = (params: GridRenderCellParams<RowInfo>) => {
|
|
151
|
+
if (params.rowNode.type !== "group" || params.value === undefined) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return <div><b>{params.value}</b></div>;
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### 8. Duplicate Tree Building Logic
|
|
161
|
+
|
|
162
|
+
**File:** `TreeView/treeViewHelpers.tsx:73-243`
|
|
163
|
+
|
|
164
|
+
`buildSortedAssayTreeView` and `buildTreeView` share common patterns.
|
|
165
|
+
|
|
166
|
+
**Fix - Extract shared logic:**
|
|
167
|
+
```typescript
|
|
168
|
+
function getSelectedRows(
|
|
169
|
+
selectedIds: string[],
|
|
170
|
+
rowById: Map<string, RowInfo>
|
|
171
|
+
): RowInfo[] {
|
|
172
|
+
return selectedIds.reduce<RowInfo[]>((acc, id) => {
|
|
173
|
+
const row = rowById.get(id);
|
|
174
|
+
if (row) acc.push(row);
|
|
175
|
+
return acc;
|
|
176
|
+
}, []);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Unused Code
|
|
183
|
+
|
|
184
|
+
### 9. Unused `CustomToolbar` Component
|
|
185
|
+
|
|
186
|
+
**File:** `DataGrid/CustomToolbar.tsx`
|
|
187
|
+
|
|
188
|
+
Entire file defines a component that is never imported or used.
|
|
189
|
+
|
|
190
|
+
**Recommendation:** Delete the file or integrate into `DataGridWrapper.tsx`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### 10. Unused Exports in consts.ts
|
|
195
|
+
|
|
196
|
+
**File:** `consts.ts:102-108`
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
export const isTrackId = (id: string): boolean => rowById.has(id);
|
|
200
|
+
export const getActiveTracks = (selectedIds: Set<string>): Set<string> =>
|
|
201
|
+
new Set(Array.from(selectedIds).filter(isTrackId));
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Never used anywhere.
|
|
205
|
+
|
|
206
|
+
**Recommendation:** Remove or document intended usage.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Type Safety Issues
|
|
211
|
+
|
|
212
|
+
### 11. Using `any` Type in getNestedValue
|
|
213
|
+
|
|
214
|
+
**File:** `DataGrid/dataGridHelpers.tsx:65-67`
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
function getNestedValue(obj: any, path: string): any {
|
|
218
|
+
return path.split(".").reduce((acc, key) => acc && acc[key], obj);
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Fix:** Remove function and access properties directly:
|
|
223
|
+
```typescript
|
|
224
|
+
const data = tracksData.tracks;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### 12. Non-Null Assertion Without Safety Check
|
|
230
|
+
|
|
231
|
+
**File:** `TreeView/TreeViewWrapper.tsx:87`
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
({items[0].allRowInfo!.length} search results)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Fix:**
|
|
238
|
+
```typescript
|
|
239
|
+
({items[0]?.allRowInfo?.length ?? 0} search results)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### 13. Implicit Any in FuseOptionKey
|
|
245
|
+
|
|
246
|
+
**File:** `types.ts:14`
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
keyWeightMap: FuseOptionKey<any>[];
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Fix:**
|
|
253
|
+
```typescript
|
|
254
|
+
keyWeightMap: FuseOptionKey<TrackInfo | RowInfo>[];
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Performance Issues
|
|
260
|
+
|
|
261
|
+
### 14. Redundant useEffect Dependency
|
|
262
|
+
|
|
263
|
+
**File:** `DataGrid/DataGridWrapper.tsx:48-50`
|
|
264
|
+
|
|
265
|
+
**Fix - Memoize baseVisibility:**
|
|
266
|
+
```typescript
|
|
267
|
+
const baseVisibility = useMemo<GridColumnVisibilityModel>(() =>
|
|
268
|
+
sortedAssay
|
|
269
|
+
? { assay: false, ontology: false, displayname: false, id: false }
|
|
270
|
+
: { ontology: false, displayname: false, assay: false, id: false },
|
|
271
|
+
[sortedAssay]
|
|
272
|
+
);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### 15. Eager Module Initialization
|
|
278
|
+
|
|
279
|
+
**File:** `consts.ts:95-97`
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const humanData = buildRowsForAssembly("GRCh38");
|
|
283
|
+
export const rows = humanData.rows;
|
|
284
|
+
export const rowById = humanData.rowById;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Runs at module load time even if never used.
|
|
288
|
+
|
|
289
|
+
**Fix - Lazy initialization:**
|
|
290
|
+
```typescript
|
|
291
|
+
let _humanData: ReturnType<typeof buildRowsForAssembly> | null = null;
|
|
292
|
+
|
|
293
|
+
function getHumanData() {
|
|
294
|
+
if (!_humanData) {
|
|
295
|
+
_humanData = buildRowsForAssembly("GRCh38");
|
|
296
|
+
}
|
|
297
|
+
return _humanData;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Code Quality Issues
|
|
304
|
+
|
|
305
|
+
### 16. Magic Number for maxTracks
|
|
306
|
+
|
|
307
|
+
**File:** `store.ts:19`
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
maxTracks: 30,
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Fix:**
|
|
314
|
+
```typescript
|
|
315
|
+
const DEFAULT_MAX_TRACKS = 30;
|
|
316
|
+
maxTracks: DEFAULT_MAX_TRACKS,
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### 17. Redundant Null Check
|
|
322
|
+
|
|
323
|
+
**File:** `TreeView/treeViewHelpers.tsx:191-194`
|
|
324
|
+
|
|
325
|
+
The `reduce` already filters out falsy values, so the check in `forEach` is redundant.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
### 18. Redundant has() + get()
|
|
330
|
+
|
|
331
|
+
**File:** `store.ts:36-43`
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
if (storeRowById.has(id)) {
|
|
335
|
+
const row = storeRowById.get(id);
|
|
336
|
+
if (row) { // Redundant after has()
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Fix:**
|
|
340
|
+
```typescript
|
|
341
|
+
const row = storeRowById.get(id);
|
|
342
|
+
if (row) {
|
|
343
|
+
map.set(id, row);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
### 19. Commented-Out Code
|
|
350
|
+
|
|
351
|
+
**File:** `store.ts:9-10`
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// const isAutoGeneratedId = (id: string) => id.startsWith("auto-generated-row-");
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Recommendation:** Remove or use.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
### 20. Typo in Comment
|
|
362
|
+
|
|
363
|
+
**File:** `types.ts:22`
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
* Types for the JSON-formatted tracks fomr modifiedHumanTracks.json
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Should be "from".
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
### 21. Missing Return Type Annotations
|
|
374
|
+
|
|
375
|
+
**File:** `TreeView/treeViewHelpers.tsx:288`
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
export function AssayIcon(type: string) {
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Fix:**
|
|
382
|
+
```typescript
|
|
383
|
+
export function AssayIcon(type: string): JSX.Element {
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
### 22. Hardcoded Color Values
|
|
389
|
+
|
|
390
|
+
**File:** `TreeView/treeViewHelpers.tsx:289-298`
|
|
391
|
+
|
|
392
|
+
**Recommendation:** Move to `consts.ts` as `ASSAY_COLORS` for reusability.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Summary
|
|
397
|
+
|
|
398
|
+
| Severity | Count | Fixed |
|
|
399
|
+
|----------|-------|-------|
|
|
400
|
+
| Critical | 4 | 1 |
|
|
401
|
+
| High | 4 | 0 |
|
|
402
|
+
| Medium | 5 | 0 |
|
|
403
|
+
| Low | 9 | 0 |
|
|
404
|
+
| **Total** | **22** | **1** |
|
package/src/TrackSelect/store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { create, StoreApi, UseBoundStore } from "zustand";
|
|
2
|
-
import {
|
|
2
|
+
import { buildRowsForAssembly, Assembly } from "./consts";
|
|
3
3
|
import { RowInfo, SelectionAction, SelectionState } from "./types";
|
|
4
4
|
|
|
5
5
|
export type SelectionStoreInstance = UseBoundStore<
|
|
@@ -7,25 +7,35 @@ export type SelectionStoreInstance = UseBoundStore<
|
|
|
7
7
|
>;
|
|
8
8
|
|
|
9
9
|
// Helper to check if an ID is auto-generated by DataGrid grouping
|
|
10
|
-
const isAutoGeneratedId = (id: string) => id.startsWith("auto-generated-row-");
|
|
10
|
+
// const isAutoGeneratedId = (id: string) => id.startsWith("auto-generated-row-");
|
|
11
|
+
|
|
12
|
+
export function createSelectionStore(
|
|
13
|
+
assembly: Assembly,
|
|
14
|
+
initialIds?: Set<string>,
|
|
15
|
+
) {
|
|
16
|
+
const { rows, rowById } = buildRowsForAssembly(assembly);
|
|
11
17
|
|
|
12
|
-
export function createSelectionStore(initialIds?: Set<string>) {
|
|
13
18
|
return create<SelectionState & SelectionAction>((set, get) => ({
|
|
14
19
|
maxTracks: 30,
|
|
20
|
+
assembly,
|
|
21
|
+
rows,
|
|
22
|
+
rowById,
|
|
15
23
|
// Stores ALL selected IDs, including auto-generated group IDs
|
|
16
24
|
selectedIds: initialIds ? new Set(initialIds) : new Set<string>(),
|
|
17
25
|
// Returns only real track IDs (filters out auto-generated group IDs)
|
|
18
26
|
getTrackIds: () => {
|
|
19
27
|
const all = get().selectedIds;
|
|
20
|
-
|
|
28
|
+
const storeRowById = get().rowById;
|
|
29
|
+
return new Set([...all].filter((id) => storeRowById.has(id)));
|
|
21
30
|
},
|
|
22
31
|
// Returns a Map of track IDs to RowInfo (no auto-generated IDs)
|
|
23
32
|
getTrackMap: () => {
|
|
24
33
|
const all = get().selectedIds;
|
|
34
|
+
const storeRowById = get().rowById;
|
|
25
35
|
const map = new Map<string, RowInfo>();
|
|
26
36
|
all.forEach((id) => {
|
|
27
|
-
if (
|
|
28
|
-
const row =
|
|
37
|
+
if (storeRowById.has(id)) {
|
|
38
|
+
const row = storeRowById.get(id);
|
|
29
39
|
if (row) {
|
|
30
40
|
map.set(id, row);
|
|
31
41
|
}
|
package/src/TrackSelect/types.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface SearchTracksProps {
|
|
|
22
22
|
* Types for the JSON-formatted tracks fomr modifiedHumanTracks.json
|
|
23
23
|
*/
|
|
24
24
|
export type AssayInfo = {
|
|
25
|
+
id: string;
|
|
25
26
|
assay: string;
|
|
26
27
|
url: string;
|
|
27
28
|
experimentAccession: string;
|
|
@@ -41,6 +42,7 @@ export type TrackInfo = {
|
|
|
41
42
|
* Row format for DataGrid
|
|
42
43
|
*/
|
|
43
44
|
export type RowInfo = {
|
|
45
|
+
id: string;
|
|
44
46
|
ontology: string;
|
|
45
47
|
lifeStage: string;
|
|
46
48
|
sampleType: string;
|
|
@@ -98,6 +100,12 @@ export interface CustomTreeItemProps
|
|
|
98
100
|
*/
|
|
99
101
|
export type SelectionState = {
|
|
100
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>;
|
|
101
109
|
// All selected IDs including auto-generated group IDs from DataGrid
|
|
102
110
|
selectedIds: Set<string>;
|
|
103
111
|
};
|