@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.
Files changed (35) hide show
  1. package/dist/TrackSelect/Data/{modifiedHumanTracks.json.d.ts → humanBiosamples.json.d.ts} +40705 -20804
  2. package/dist/TrackSelect/Data/mouseBiosamples.json.d.ts +10346 -0
  3. package/dist/TrackSelect/DataGrid/GroupingCell.d.ts +2 -0
  4. package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +25 -6
  5. package/dist/TrackSelect/TrackSelect.d.ts +4 -1
  6. package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +1 -1
  7. package/dist/TrackSelect/consts.d.ts +6 -17
  8. package/dist/TrackSelect/store.d.ts +2 -1
  9. package/dist/TrackSelect/types.d.ts +5 -0
  10. package/dist/genomebrowser-ui.es.js +1173 -951
  11. package/dist/genomebrowser-ui.es.js.map +1 -1
  12. package/dist/lib.d.ts +0 -2
  13. package/package.json +2 -2
  14. package/src/TrackSelect/Data/formatBiosamples.go +254 -0
  15. package/src/TrackSelect/Data/{modifiedHumanTracks.json → humanBiosamples.json} +40704 -20804
  16. package/src/TrackSelect/Data/mouseBiosamples.json +10343 -0
  17. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +13 -6
  18. package/src/TrackSelect/DataGrid/GroupingCell.tsx +144 -0
  19. package/src/TrackSelect/DataGrid/columns.tsx +7 -0
  20. package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +64 -19
  21. package/src/TrackSelect/TrackSelect.tsx +86 -27
  22. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +1 -1
  23. package/src/TrackSelect/TreeView/treeViewHelpers.tsx +65 -17
  24. package/src/TrackSelect/consts.ts +30 -30
  25. package/src/TrackSelect/issues.md +404 -0
  26. package/src/TrackSelect/store.ts +16 -6
  27. package/src/TrackSelect/types.ts +8 -0
  28. package/src/lib.ts +0 -3
  29. package/test/main.tsx +399 -17
  30. package/dist/TrackSelect/treeViewHelpers.d.ts +0 -1
  31. package/src/TrackSelect/.claude/settings.local.json +0 -7
  32. package/src/TrackSelect/Data/humanTracks.json +0 -35711
  33. package/src/TrackSelect/Data/human_chromhmm_biosamples_with_all_urls.json +0 -35716
  34. package/src/TrackSelect/bug.md +0 -4
  35. 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.experimentAccession}
78
+ getRowId={(row) => row.id}
79
79
  autosizeOptions={autosizeOptions}
80
80
  rowGroupingModel={groupingModel}
81
- groupingColDef={{ leafField, display: "flex" }}
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 tracksData from "../Data/modifiedHumanTracks.json";
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 === assay.toLowerCase()) ||
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 or FuseResult into RowInfo for DataGrid display.
55
- * @param track TrackInfo object or FuseResult containing information from JSON file
56
- * @returns Flattened RowInfo object
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 flattenIntoRow(track: TrackInfo): RowInfo {
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
- ontology: capitalize(ontology),
64
- lifeStage: capitalize(lifeStage),
65
- sampleType: capitalize(sampleType),
66
- displayname: capitalize(displayname),
67
- assay: formatAssayType(assay),
68
- experimentAccession,
69
- fileAccession,
70
- url,
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
- }: SearchTracksProps): FuseResult<TrackInfo>[] {
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 { flattenIntoRow, searchTracks } from "./DataGrid/dataGridHelpers";
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({ store }: TrackSelectProps) {
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 trackIds = useMemo(() => getTrackIds(), [selectedIds, getTrackIds]);
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(trackIds),
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(trackIds),
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
- }, [trackIds, sortedAssay]);
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(trackIds).filter((id) =>
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, trackIds, sortedAssay]);
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: SearchTracksProps = {
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
- .map(flattenIntoRow);
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.experimentAccession);
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
- // Store ALL IDs (including auto-generated group IDs)
243
- setSelected(allIds);
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={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={trackIds}
325
+ trackIds={workingTrackIds}
283
326
  isSearchResult={isSearchResult}
284
327
  />
285
328
  </Box>
286
329
  </Stack>
287
- <Box sx={{ justifyContent: "flex-end" }}>
330
+ <Box
331
+ sx={{ display: "flex", justifyContent: "space-between", mt: 2, gap: 2 }}
332
+ >
288
333
  <Button
289
- variant="contained"
290
- color="primary"
291
- onClick={clear}
292
- sx={{ mt: 2, justifyContent: "flex-end" }}
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
- Clear Selection
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>,