@weng-lab/genomebrowser-ui 0.1.8 → 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 -950
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/dist/lib.d.ts +2 -0
- 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 +3 -0
- package/test/main.tsx +419 -4
- 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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { useEffect, useState } from "react";
|
|
11
11
|
import { DataGridProps } from "../types";
|
|
12
12
|
import { defaultColumns, sortedByAssayColumns } from "./columns";
|
|
13
|
+
import GroupingCell from "./GroupingCell";
|
|
13
14
|
|
|
14
15
|
const autosizeOptions: GridAutosizeOptions = {
|
|
15
16
|
expand: true,
|
|
@@ -17,7 +18,6 @@ const autosizeOptions: GridAutosizeOptions = {
|
|
|
17
18
|
outliersFactor: 1.5,
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
// TODO: figure out where mui stores the number of rows in a row grouping so that can be bolded too
|
|
21
21
|
export function DataGridWrapper(props: DataGridProps) {
|
|
22
22
|
const { sortedAssay, handleSelection, rows, selectedIds } = props;
|
|
23
23
|
|
|
@@ -36,10 +36,10 @@ export function DataGridWrapper(props: DataGridProps) {
|
|
|
36
36
|
const columnModel = sortedAssay ? sortedByAssayColumns : defaultColumns;
|
|
37
37
|
const leafField = sortedAssay ? "displayname" : "assay";
|
|
38
38
|
|
|
39
|
-
// Hide columns that are used in grouping or as leaf field
|
|
39
|
+
// Hide columns that are used in grouping or as leaf field, plus ID column
|
|
40
40
|
const baseVisibility: GridColumnVisibilityModel = sortedAssay
|
|
41
|
-
? { assay: false, ontology: false, displayname: false } // sort by assay: assay & ontology are grouping, displayname is leaf
|
|
42
|
-
: { ontology: false, displayname: false, assay: false }; // default: ontology & displayname are grouping, assay is leaf
|
|
41
|
+
? { assay: false, ontology: false, displayname: false, id: false } // sort by assay: assay & ontology are grouping, displayname is leaf
|
|
42
|
+
: { ontology: false, displayname: false, assay: false, id: false }; // default: ontology & displayname are grouping, assay is leaf
|
|
43
43
|
|
|
44
44
|
const [columnVisibilityModel, setColumnVisibilityModel] =
|
|
45
45
|
useState<GridColumnVisibilityModel>(baseVisibility);
|
|
@@ -75,10 +75,17 @@ export function DataGridWrapper(props: DataGridProps) {
|
|
|
75
75
|
apiRef={apiRef}
|
|
76
76
|
rows={rows}
|
|
77
77
|
columns={columnModel}
|
|
78
|
-
getRowId={(row) => row.
|
|
78
|
+
getRowId={(row) => row.id}
|
|
79
79
|
autosizeOptions={autosizeOptions}
|
|
80
80
|
rowGroupingModel={groupingModel}
|
|
81
|
-
groupingColDef={{
|
|
81
|
+
groupingColDef={{
|
|
82
|
+
leafField,
|
|
83
|
+
display: "flex",
|
|
84
|
+
minWidth: 300,
|
|
85
|
+
maxWidth: 500,
|
|
86
|
+
flex: 2,
|
|
87
|
+
renderCell: (params) => <GroupingCell {...params} />,
|
|
88
|
+
}}
|
|
82
89
|
columnVisibilityModel={columnVisibilityModel}
|
|
83
90
|
onColumnVisibilityModelChange={setColumnVisibilityModel}
|
|
84
91
|
onRowSelectionModelChange={handleSelection}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Stack, Tooltip, Box, IconButton } from "@mui/material";
|
|
2
|
+
import {
|
|
3
|
+
GridRenderCellParams,
|
|
4
|
+
useGridApiContext,
|
|
5
|
+
GridGroupNode,
|
|
6
|
+
} from "@mui/x-data-grid-premium";
|
|
7
|
+
import { assayTypes } from "../consts";
|
|
8
|
+
import { AssayIcon } from "../TreeView/treeViewHelpers";
|
|
9
|
+
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
10
|
+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
11
|
+
|
|
12
|
+
// Custom grouping cell that preserves expand/collapse while adding truncation + tooltip
|
|
13
|
+
export default function GroupingCell(params: GridRenderCellParams) {
|
|
14
|
+
const apiRef = useGridApiContext();
|
|
15
|
+
const isGroup = params.rowNode.type === "group";
|
|
16
|
+
const groupNode = params.rowNode as GridGroupNode;
|
|
17
|
+
const isExpanded = isGroup ? groupNode.childrenExpanded : false;
|
|
18
|
+
const groupingField = isGroup ? groupNode.groupingField : null;
|
|
19
|
+
const depth = params.rowNode.depth ?? 0;
|
|
20
|
+
|
|
21
|
+
const handleExpandClick = (e: React.MouseEvent) => {
|
|
22
|
+
e.stopPropagation();
|
|
23
|
+
apiRef.current.setRowChildrenExpansion(params.id, !isExpanded);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Render content based on grouping field
|
|
27
|
+
const renderContent = () => {
|
|
28
|
+
const value = String(params.value ?? "");
|
|
29
|
+
|
|
30
|
+
// For assay groups, show the colored icon
|
|
31
|
+
if (isGroup && groupingField === "assay") {
|
|
32
|
+
return (
|
|
33
|
+
<Stack
|
|
34
|
+
direction="row"
|
|
35
|
+
spacing={1}
|
|
36
|
+
alignItems="center"
|
|
37
|
+
sx={{ flex: 1, overflow: "hidden" }}
|
|
38
|
+
>
|
|
39
|
+
{AssayIcon(value)}
|
|
40
|
+
<Tooltip title={value} placement="top-start" enterDelay={500}>
|
|
41
|
+
<Box
|
|
42
|
+
sx={{
|
|
43
|
+
overflow: "hidden",
|
|
44
|
+
textOverflow: "ellipsis",
|
|
45
|
+
whiteSpace: "nowrap",
|
|
46
|
+
fontWeight: "bold",
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{params.formattedValue}
|
|
50
|
+
</Box>
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</Stack>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// For other groups (ontology, displayname), show bold text
|
|
57
|
+
if (isGroup) {
|
|
58
|
+
return (
|
|
59
|
+
<Tooltip title={value} placement="top-start" enterDelay={500}>
|
|
60
|
+
<Box
|
|
61
|
+
sx={{
|
|
62
|
+
overflow: "hidden",
|
|
63
|
+
textOverflow: "ellipsis",
|
|
64
|
+
whiteSpace: "nowrap",
|
|
65
|
+
flex: 1,
|
|
66
|
+
fontWeight: "bold",
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{params.formattedValue}
|
|
70
|
+
</Box>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// For leaf rows - check if it's an assay value
|
|
76
|
+
const isAssayValue = assayTypes
|
|
77
|
+
.map((a) => a.toLowerCase())
|
|
78
|
+
.includes(value.toLowerCase());
|
|
79
|
+
|
|
80
|
+
if (isAssayValue) {
|
|
81
|
+
return (
|
|
82
|
+
<Stack
|
|
83
|
+
direction="row"
|
|
84
|
+
spacing={1}
|
|
85
|
+
alignItems="center"
|
|
86
|
+
sx={{ flex: 1, overflow: "hidden" }}
|
|
87
|
+
>
|
|
88
|
+
{AssayIcon(value)}
|
|
89
|
+
<Tooltip title={value} placement="top-start" enterDelay={500}>
|
|
90
|
+
<Box
|
|
91
|
+
sx={{
|
|
92
|
+
overflow: "hidden",
|
|
93
|
+
textOverflow: "ellipsis",
|
|
94
|
+
whiteSpace: "nowrap",
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{params.formattedValue}
|
|
98
|
+
</Box>
|
|
99
|
+
</Tooltip>
|
|
100
|
+
</Stack>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Tooltip title={value} placement="top-start" enterDelay={500}>
|
|
106
|
+
<Box
|
|
107
|
+
sx={{
|
|
108
|
+
overflow: "hidden",
|
|
109
|
+
textOverflow: "ellipsis",
|
|
110
|
+
whiteSpace: "nowrap",
|
|
111
|
+
flex: 1,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{params.formattedValue}
|
|
115
|
+
</Box>
|
|
116
|
+
</Tooltip>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Indent based on depth (2 units per level)
|
|
121
|
+
const indentLevel = depth * 2;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Box
|
|
125
|
+
sx={{
|
|
126
|
+
display: "flex",
|
|
127
|
+
alignItems: "center",
|
|
128
|
+
width: "100%",
|
|
129
|
+
ml: indentLevel,
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{isGroup && (
|
|
133
|
+
<IconButton size="small" onClick={handleExpandClick} sx={{ mr: 0.5 }}>
|
|
134
|
+
{isExpanded ? (
|
|
135
|
+
<ExpandMoreIcon fontSize="small" />
|
|
136
|
+
) : (
|
|
137
|
+
<ChevronRightIcon fontSize="small" />
|
|
138
|
+
)}
|
|
139
|
+
</IconButton>
|
|
140
|
+
)}
|
|
141
|
+
{renderContent()}
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -123,6 +123,11 @@ const fileCol: GridColDef<RowInfo> = {
|
|
|
123
123
|
headerName: "File Accession",
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
+
const idCol: GridColDef<RowInfo> = {
|
|
127
|
+
field: "id",
|
|
128
|
+
headerName: "ID",
|
|
129
|
+
};
|
|
130
|
+
|
|
126
131
|
export const sortedByAssayColumns: GridColDef<RowInfo>[] = [
|
|
127
132
|
displayNameCol,
|
|
128
133
|
sortedByAssayOntologyCol,
|
|
@@ -131,6 +136,7 @@ export const sortedByAssayColumns: GridColDef<RowInfo>[] = [
|
|
|
131
136
|
sortedByAssayAssayCol,
|
|
132
137
|
experimentCol,
|
|
133
138
|
fileCol,
|
|
139
|
+
idCol,
|
|
134
140
|
];
|
|
135
141
|
|
|
136
142
|
export const defaultColumns: GridColDef<RowInfo>[] = [
|
|
@@ -141,4 +147,5 @@ export const defaultColumns: GridColDef<RowInfo>[] = [
|
|
|
141
147
|
displayNameCol,
|
|
142
148
|
experimentCol,
|
|
143
149
|
fileCol,
|
|
150
|
+
idCol,
|
|
144
151
|
];
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { capitalize } from "@mui/material";
|
|
2
2
|
import Fuse, { FuseResult } from "fuse.js";
|
|
3
|
-
import
|
|
3
|
+
import humanTracksData from "../Data/humanBiosamples.json";
|
|
4
|
+
import mouseTracksData from "../Data/mouseBiosamples.json";
|
|
4
5
|
import { AssayInfo, RowInfo, SearchTracksProps, TrackInfo } from "../types";
|
|
6
|
+
import { Assembly } from "../consts";
|
|
7
|
+
|
|
8
|
+
const tracksDataByAssembly: Record<Assembly, typeof humanTracksData> = {
|
|
9
|
+
GRCh38: humanTracksData,
|
|
10
|
+
mm10: mouseTracksData,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function getTracksData(assembly: Assembly) {
|
|
14
|
+
return tracksDataByAssembly[assembly];
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
function formatAssayType(assay: string): string {
|
|
7
18
|
switch (assay) {
|
|
@@ -17,11 +28,39 @@ function formatAssayType(assay: string): string {
|
|
|
17
28
|
return "CTCF";
|
|
18
29
|
case "chromhmm":
|
|
19
30
|
return "ChromHMM";
|
|
31
|
+
case "ccre":
|
|
32
|
+
return "cCRE";
|
|
33
|
+
case "rnaseq":
|
|
34
|
+
return "RNA-seq";
|
|
20
35
|
default:
|
|
21
36
|
return assay;
|
|
22
37
|
}
|
|
23
38
|
}
|
|
24
39
|
|
|
40
|
+
/** Convert display assay name to JSON format */
|
|
41
|
+
function toJsonAssayType(displayName: string): string {
|
|
42
|
+
switch (displayName.toLowerCase()) {
|
|
43
|
+
case "dnase":
|
|
44
|
+
return "dnase";
|
|
45
|
+
case "atac":
|
|
46
|
+
return "atac";
|
|
47
|
+
case "h3k4me3":
|
|
48
|
+
return "h3k4me3";
|
|
49
|
+
case "h3k27ac":
|
|
50
|
+
return "h3k27ac";
|
|
51
|
+
case "ctcf":
|
|
52
|
+
return "ctcf";
|
|
53
|
+
case "chromhmm":
|
|
54
|
+
return "chromhmm";
|
|
55
|
+
case "ccre":
|
|
56
|
+
return "ccre";
|
|
57
|
+
case "rna-seq":
|
|
58
|
+
return "rnaseq";
|
|
59
|
+
default:
|
|
60
|
+
return displayName.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
// use to get nested data in JSON file
|
|
26
65
|
function getNestedValue(obj: any, path: string): any {
|
|
27
66
|
return path.split(".").reduce((acc, key) => acc && acc[key], obj);
|
|
@@ -30,14 +69,15 @@ function getNestedValue(obj: any, path: string): any {
|
|
|
30
69
|
export function getTracksByAssayAndOntology(
|
|
31
70
|
assay: string,
|
|
32
71
|
ontology: string,
|
|
72
|
+
tracksData: ReturnType<typeof getTracksData>,
|
|
33
73
|
): TrackInfo[] {
|
|
34
74
|
let res: TrackInfo[] = [];
|
|
35
75
|
const data = getNestedValue(tracksData, "tracks");
|
|
76
|
+
const jsonAssay = toJsonAssayType(assay);
|
|
36
77
|
|
|
37
78
|
data.forEach((track: TrackInfo) => {
|
|
38
79
|
const filteredAssays =
|
|
39
|
-
track.assays?.filter((e: AssayInfo) => e.assay ===
|
|
40
|
-
[];
|
|
80
|
+
track.assays?.filter((e: AssayInfo) => e.assay === jsonAssay) || [];
|
|
41
81
|
if (
|
|
42
82
|
filteredAssays.length > 0 &&
|
|
43
83
|
track.ontology === ontology.toLowerCase()
|
|
@@ -51,24 +91,26 @@ export function getTracksByAssayAndOntology(
|
|
|
51
91
|
return res;
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
/** Flatten TrackInfo
|
|
55
|
-
* @param track TrackInfo object
|
|
56
|
-
* @returns
|
|
94
|
+
/** Flatten TrackInfo into RowInfo objects for DataGrid display.
|
|
95
|
+
* @param track TrackInfo object containing information from JSON file
|
|
96
|
+
* @returns Array of flattened RowInfo objects, one per assay
|
|
57
97
|
*/
|
|
58
|
-
export function
|
|
98
|
+
export function flattenIntoRows(track: TrackInfo): RowInfo[] {
|
|
59
99
|
const { ontology, lifeStage, sampleType, displayname } = track;
|
|
60
|
-
const { assay, experimentAccession, fileAccession, url } = track.assays[0];
|
|
61
100
|
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
return track.assays.map(
|
|
102
|
+
({ id, assay, experimentAccession, fileAccession, url }) => ({
|
|
103
|
+
id,
|
|
104
|
+
ontology: capitalize(ontology),
|
|
105
|
+
lifeStage: capitalize(lifeStage),
|
|
106
|
+
sampleType: capitalize(sampleType),
|
|
107
|
+
displayname: capitalize(displayname),
|
|
108
|
+
assay: formatAssayType(assay),
|
|
109
|
+
experimentAccession,
|
|
110
|
+
fileAccession,
|
|
111
|
+
url,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
72
114
|
}
|
|
73
115
|
|
|
74
116
|
/**
|
|
@@ -97,7 +139,10 @@ export function searchTracks({
|
|
|
97
139
|
query,
|
|
98
140
|
keyWeightMap,
|
|
99
141
|
threshold = 0.75,
|
|
100
|
-
|
|
142
|
+
tracksData,
|
|
143
|
+
}: SearchTracksProps & {
|
|
144
|
+
tracksData: ReturnType<typeof getTracksData>;
|
|
145
|
+
}): FuseResult<TrackInfo>[] {
|
|
101
146
|
const data = getNestedValue(tracksData, jsonStructure ?? "");
|
|
102
147
|
|
|
103
148
|
const fuse = new Fuse(data, {
|
|
@@ -15,39 +15,70 @@ import { GridRowSelectionModel } from "@mui/x-data-grid";
|
|
|
15
15
|
import { TreeViewBaseItem } from "@mui/x-tree-view";
|
|
16
16
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
17
17
|
import { DataGridWrapper } from "./DataGrid/DataGridWrapper";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
flattenIntoRows,
|
|
20
|
+
searchTracks,
|
|
21
|
+
getTracksData,
|
|
22
|
+
} from "./DataGrid/dataGridHelpers";
|
|
19
23
|
import { TreeViewWrapper } from "./TreeView/TreeViewWrapper";
|
|
20
24
|
import {
|
|
21
25
|
buildSortedAssayTreeView,
|
|
22
26
|
buildTreeView,
|
|
23
27
|
searchTreeItems,
|
|
24
28
|
} from "./TreeView/treeViewHelpers";
|
|
25
|
-
import { rowById, rows } from "./consts";
|
|
26
29
|
import { SelectionStoreInstance } from "./store";
|
|
27
30
|
import { ExtendedTreeItemProps, SearchTracksProps } from "./types";
|
|
28
31
|
|
|
29
32
|
export interface TrackSelectProps {
|
|
30
33
|
store: SelectionStoreInstance;
|
|
34
|
+
onSubmit?: (trackIds: Set<string>) => void;
|
|
35
|
+
onCancel?: () => void;
|
|
36
|
+
onReset?: () => void;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
export default function TrackSelect({
|
|
39
|
+
export default function TrackSelect({
|
|
40
|
+
store,
|
|
41
|
+
onSubmit,
|
|
42
|
+
onCancel,
|
|
43
|
+
onReset,
|
|
44
|
+
}: TrackSelectProps) {
|
|
34
45
|
const [limitDialogOpen, setLimitDialogOpen] = useState(false);
|
|
35
46
|
const [sortedAssay, setSortedAssay] = useState(false);
|
|
36
47
|
const [searchQuery, setSearchQuery] = useState("");
|
|
37
48
|
const [isSearchResult, setIsSearchResult] = useState(false);
|
|
38
49
|
const selectedIds = store((s) => s.selectedIds);
|
|
39
|
-
const getTrackIds = store((s) => s.getTrackIds);
|
|
40
50
|
const setSelected = store((s) => s.setSelected);
|
|
41
51
|
const clear = store((s) => s.clear);
|
|
42
52
|
const MAX_ACTIVE = store((s) => s.maxTracks);
|
|
53
|
+
const rows = store((s) => s.rows);
|
|
54
|
+
const rowById = store((s) => s.rowById);
|
|
55
|
+
const assembly = store((s) => s.assembly);
|
|
56
|
+
|
|
57
|
+
// Local working state - changes here don't affect the store until Submit
|
|
58
|
+
const [workingIds, setWorkingIds] = useState<Set<string>>(
|
|
59
|
+
() => new Set(selectedIds),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Get tracks data for search functionality
|
|
63
|
+
const tracksData = useMemo(
|
|
64
|
+
() => getTracksData(assembly as "GRCh38" | "mm10"),
|
|
65
|
+
[assembly],
|
|
66
|
+
);
|
|
43
67
|
|
|
44
|
-
// Get only real track IDs (no auto-generated group IDs)
|
|
45
|
-
const
|
|
68
|
+
// Get only real track IDs from working selection (no auto-generated group IDs)
|
|
69
|
+
const workingTrackIds = useMemo(() => {
|
|
70
|
+
return new Set([...workingIds].filter((id) => rowById.has(id)));
|
|
71
|
+
}, [workingIds, rowById]);
|
|
72
|
+
|
|
73
|
+
// Sync workingIds when store's selectedIds changes externally
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setWorkingIds(new Set(selectedIds));
|
|
76
|
+
}, [selectedIds]);
|
|
46
77
|
|
|
47
78
|
const treeItems = useMemo(() => {
|
|
48
79
|
return sortedAssay
|
|
49
80
|
? buildSortedAssayTreeView(
|
|
50
|
-
Array.from(
|
|
81
|
+
Array.from(workingTrackIds),
|
|
51
82
|
{
|
|
52
83
|
id: "1",
|
|
53
84
|
isAssayItem: false,
|
|
@@ -59,7 +90,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
59
90
|
rowById,
|
|
60
91
|
)
|
|
61
92
|
: buildTreeView(
|
|
62
|
-
Array.from(
|
|
93
|
+
Array.from(workingTrackIds),
|
|
63
94
|
{
|
|
64
95
|
id: "1",
|
|
65
96
|
isAssayItem: false,
|
|
@@ -70,7 +101,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
70
101
|
},
|
|
71
102
|
rowById,
|
|
72
103
|
);
|
|
73
|
-
}, [
|
|
104
|
+
}, [workingTrackIds, sortedAssay, rowById]);
|
|
74
105
|
|
|
75
106
|
const [filteredRows, setFilteredRows] = useState(rows);
|
|
76
107
|
const [filteredTreeItems, setFilteredTreeItems] = useState([
|
|
@@ -94,7 +125,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
94
125
|
searchResultIdsRef.current = new Set();
|
|
95
126
|
} else if (searchResultIdsRef.current.size > 0) {
|
|
96
127
|
// When selection changes during search, rebuild tree from selected items that match search
|
|
97
|
-
const matchingTrackIds = Array.from(
|
|
128
|
+
const matchingTrackIds = Array.from(workingTrackIds).filter((id) =>
|
|
98
129
|
searchResultIdsRef.current.has(id),
|
|
99
130
|
);
|
|
100
131
|
|
|
@@ -126,7 +157,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
126
157
|
|
|
127
158
|
setFilteredTreeItems(newTreeItems);
|
|
128
159
|
}
|
|
129
|
-
}, [treeItems, searchQuery,
|
|
160
|
+
}, [treeItems, searchQuery, workingTrackIds, sortedAssay, rowById, rows]);
|
|
130
161
|
|
|
131
162
|
const handleToggle = () => {
|
|
132
163
|
setSortedAssay(!sortedAssay);
|
|
@@ -147,7 +178,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
147
178
|
return; // useEffect handles empty query
|
|
148
179
|
}
|
|
149
180
|
|
|
150
|
-
const dataGridSearchProps
|
|
181
|
+
const dataGridSearchProps = {
|
|
151
182
|
jsonStructure: "tracks",
|
|
152
183
|
query: query,
|
|
153
184
|
keyWeightMap: [
|
|
@@ -159,6 +190,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
159
190
|
"experimentAccession",
|
|
160
191
|
"fileAccession",
|
|
161
192
|
],
|
|
193
|
+
tracksData,
|
|
162
194
|
};
|
|
163
195
|
|
|
164
196
|
const treeSearchProps: SearchTracksProps = {
|
|
@@ -176,13 +208,11 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
176
208
|
};
|
|
177
209
|
const newDataGridRows = searchTracks(dataGridSearchProps)
|
|
178
210
|
.map((t) => t.item)
|
|
179
|
-
.
|
|
211
|
+
.flatMap(flattenIntoRows);
|
|
180
212
|
|
|
181
213
|
// 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.
|
|
183
|
-
const retIds = searchTreeItems(treeSearchProps).map(
|
|
184
|
-
(r) => r.item.experimentAccession,
|
|
185
|
-
);
|
|
214
|
+
const newDataGridIds = newDataGridRows.map((r) => r.id);
|
|
215
|
+
const retIds = searchTreeItems(treeSearchProps).map((r) => r.item.id);
|
|
186
216
|
const newTreeIds = retIds.filter((i) => newDataGridIds.includes(i));
|
|
187
217
|
|
|
188
218
|
// build new tree from the newTreeIds
|
|
@@ -239,8 +269,21 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
239
269
|
return;
|
|
240
270
|
}
|
|
241
271
|
|
|
242
|
-
//
|
|
243
|
-
|
|
272
|
+
// Update working state (not the store yet)
|
|
273
|
+
setWorkingIds(allIds);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const handleSubmit = () => {
|
|
277
|
+
// Commit working selection to store
|
|
278
|
+
setSelected(workingIds);
|
|
279
|
+
// Call callback with real track IDs
|
|
280
|
+
onSubmit?.(workingTrackIds);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const handleCancel = () => {
|
|
284
|
+
// Revert working state to store's committed state
|
|
285
|
+
setWorkingIds(new Set(selectedIds));
|
|
286
|
+
onCancel?.();
|
|
244
287
|
};
|
|
245
288
|
|
|
246
289
|
return (
|
|
@@ -270,7 +313,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
270
313
|
? `${filteredRows.length} Search Results`
|
|
271
314
|
: `${rows.length} Available Tracks`
|
|
272
315
|
}
|
|
273
|
-
selectedIds={
|
|
316
|
+
selectedIds={workingIds}
|
|
274
317
|
handleSelection={handleSelection}
|
|
275
318
|
sortedAssay={sortedAssay}
|
|
276
319
|
/>
|
|
@@ -279,20 +322,36 @@ export default function TrackSelect({ store }: TrackSelectProps) {
|
|
|
279
322
|
<TreeViewWrapper
|
|
280
323
|
store={store}
|
|
281
324
|
items={filteredTreeItems}
|
|
282
|
-
trackIds={
|
|
325
|
+
trackIds={workingTrackIds}
|
|
283
326
|
isSearchResult={isSearchResult}
|
|
284
327
|
/>
|
|
285
328
|
</Box>
|
|
286
329
|
</Stack>
|
|
287
|
-
<Box
|
|
330
|
+
<Box
|
|
331
|
+
sx={{ display: "flex", justifyContent: "space-between", mt: 2, gap: 2 }}
|
|
332
|
+
>
|
|
288
333
|
<Button
|
|
289
|
-
variant="
|
|
290
|
-
color="
|
|
291
|
-
onClick={
|
|
292
|
-
|
|
334
|
+
variant="outlined"
|
|
335
|
+
color="secondary"
|
|
336
|
+
onClick={() => {
|
|
337
|
+
if (onReset) {
|
|
338
|
+
onReset();
|
|
339
|
+
} else {
|
|
340
|
+
clear();
|
|
341
|
+
setWorkingIds(new Set());
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
293
344
|
>
|
|
294
|
-
|
|
345
|
+
Reset
|
|
295
346
|
</Button>
|
|
347
|
+
<Box sx={{ display: "flex", gap: 2 }}>
|
|
348
|
+
<Button variant="outlined" onClick={handleCancel}>
|
|
349
|
+
Cancel
|
|
350
|
+
</Button>
|
|
351
|
+
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
|
352
|
+
Submit
|
|
353
|
+
</Button>
|
|
354
|
+
</Box>
|
|
296
355
|
</Box>
|
|
297
356
|
<Dialog open={limitDialogOpen} onClose={() => setLimitDialogOpen(false)}>
|
|
298
357
|
<DialogTitle>Track Limit Reached</DialogTitle>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Box, Paper, Typography } from "@mui/material";
|
|
2
2
|
import { RichTreeView, TreeViewBaseItem } from "@mui/x-tree-view";
|
|
3
|
-
import { rowById } from "../consts";
|
|
4
3
|
import {
|
|
5
4
|
CustomTreeItemProps,
|
|
6
5
|
ExtendedTreeItemProps,
|
|
@@ -16,6 +15,7 @@ export function TreeViewWrapper({
|
|
|
16
15
|
isSearchResult,
|
|
17
16
|
}: TreeViewWrapperProps) {
|
|
18
17
|
const removeIds = store((s) => s.removeIds);
|
|
18
|
+
const rowById = store((s) => s.rowById);
|
|
19
19
|
|
|
20
20
|
const handleRemoveTreeItem = (
|
|
21
21
|
item: TreeViewBaseItem<ExtendedTreeItemProps>,
|