@weng-lab/genomebrowser-ui 0.2.0 → 0.2.2

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 (57) hide show
  1. package/.env.local +1 -1
  2. package/dist/TrackSelect/Dialogs/ClearDialog.d.ts +9 -0
  3. package/dist/TrackSelect/Dialogs/LimitDialog.d.ts +7 -0
  4. package/dist/TrackSelect/Dialogs/ResetDialog.d.ts +7 -0
  5. package/dist/TrackSelect/Folders/biosamples/data/human.json.d.ts +57141 -57141
  6. package/dist/TrackSelect/Folders/biosamples/data/mouse.json.d.ts +10394 -10394
  7. package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +7 -7
  8. package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +7 -7
  9. package/dist/genomebrowser-ui.es.js +736 -652
  10. package/dist/genomebrowser-ui.es.js.map +1 -1
  11. package/eslint.config.js +30 -30
  12. package/index.html +14 -14
  13. package/package.json +2 -1
  14. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +137 -137
  15. package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -64
  16. package/src/TrackSelect/Dialogs/ClearDialog.tsx +63 -0
  17. package/src/TrackSelect/Dialogs/LimitDialog.tsx +33 -0
  18. package/src/TrackSelect/Dialogs/ResetDialog.tsx +43 -0
  19. package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -38
  20. package/src/TrackSelect/FolderList/FolderCard.tsx +51 -51
  21. package/src/TrackSelect/FolderList/FolderList.tsx +47 -47
  22. package/src/TrackSelect/Folders/NEW.md +929 -929
  23. package/src/TrackSelect/Folders/biosamples/data/formatBiosamples.go +254 -254
  24. package/src/TrackSelect/Folders/biosamples/data/human.json +57141 -57141
  25. package/src/TrackSelect/Folders/biosamples/data/mouse.json +10394 -10394
  26. package/src/TrackSelect/Folders/biosamples/human.ts +17 -17
  27. package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -17
  28. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +78 -78
  29. package/src/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.tsx +146 -146
  30. package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -15
  31. package/src/TrackSelect/Folders/biosamples/shared/columns.tsx +165 -165
  32. package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -116
  33. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -116
  34. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +224 -224
  35. package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -48
  36. package/src/TrackSelect/Folders/genes/data/human.json +7 -7
  37. package/src/TrackSelect/Folders/genes/data/mouse.json +7 -7
  38. package/src/TrackSelect/Folders/genes/human.ts +16 -16
  39. package/src/TrackSelect/Folders/genes/mouse.ts +16 -16
  40. package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -42
  41. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -68
  42. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -45
  43. package/src/TrackSelect/Folders/genes/shared/types.ts +29 -29
  44. package/src/TrackSelect/Folders/index.ts +30 -30
  45. package/src/TrackSelect/Folders/types.ts +106 -106
  46. package/src/TrackSelect/TrackSelect.tsx +82 -74
  47. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +214 -214
  48. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +145 -145
  49. package/src/TrackSelect/store.ts +117 -117
  50. package/src/TrackSelect/types.ts +121 -121
  51. package/src/lib.ts +13 -13
  52. package/src/vite-env.d.ts +1 -1
  53. package/test/main.tsx +369 -369
  54. package/tsconfig.app.json +25 -25
  55. package/tsconfig.json +7 -7
  56. package/tsconfig.node.json +25 -25
  57. package/vite.config.ts +66 -66
@@ -3,17 +3,18 @@ import {
3
3
  Box,
4
4
  Button,
5
5
  Dialog,
6
- DialogActions,
7
6
  DialogContent,
8
- DialogContentText,
9
7
  DialogTitle,
10
8
  IconButton,
11
9
  Stack,
12
10
  } from "@mui/material";
13
11
  import { TreeViewBaseItem } from "@mui/x-tree-view";
14
12
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
15
- import { Breadcrumb } from "./FolderList/Breadcrumb";
16
13
  import { DataGridWrapper } from "./DataGrid/DataGridWrapper";
14
+ import { ClearDialog } from "./Dialogs/ClearDialog";
15
+ import { LimitDialog } from "./Dialogs/LimitDialog";
16
+ import { ResetDialog } from "./Dialogs/ResetDialog";
17
+ import { Breadcrumb } from "./FolderList/Breadcrumb";
17
18
  import { FolderList } from "./FolderList/FolderList";
18
19
  import { FolderDefinition, FolderRuntimeConfig } from "./Folders/types";
19
20
  import { createSelectionStore, SelectionStoreInstance } from "./store";
@@ -87,6 +88,7 @@ export default function TrackSelect({
87
88
  }: TrackSelectProps) {
88
89
  const [limitDialogOpen, setLimitDialogOpen] = useState(false);
89
90
  const [clearDialogOpen, setClearDialogOpen] = useState(false);
91
+ const [resetDialogOpen, setResetDialogOpen] = useState(false);
90
92
  const [runtimeConfigByFolder, setRuntimeConfigByFolder] = useState(() =>
91
93
  buildRuntimeConfigMap(folders),
92
94
  );
@@ -273,6 +275,30 @@ export default function TrackSelect({
273
275
  setClearDialogOpen(true);
274
276
  };
275
277
 
278
+ const handleReset = () => {
279
+ setResetDialogOpen(true);
280
+ };
281
+
282
+ const confirmReset = () => {
283
+ setResetDialogOpen(false);
284
+ if (!initialSelection) return;
285
+
286
+ // Reset to initial selection
287
+ initialSelection.forEach((ids, folderId) => {
288
+ setSelection(folderId, new Set(ids));
289
+ });
290
+ // Clear any folders not in initialSelection
291
+ folderIds.forEach((folderId) => {
292
+ if (!initialSelection.has(folderId)) {
293
+ setSelection(folderId, new Set<string>());
294
+ }
295
+ });
296
+
297
+ const newSnapshot = cloneSelectionMap(initialSelection);
298
+ setCommittedSnapshot(newSnapshot);
299
+ onSubmit(newSnapshot);
300
+ };
301
+
276
302
  const confirmClear = () => {
277
303
  setClearDialogOpen(false);
278
304
  let newSnapshot: Map<string, Set<string>>;
@@ -322,7 +348,7 @@ export default function TrackSelect({
322
348
  <Box sx={{ p: 2 }}>No folders available.</Box>
323
349
  ) : (
324
350
  <Box sx={{ flex: 1, pt: 1 }}>
325
- {/* Toolbar row - breadcrumb on left, extras on right */}
351
+ {/* Toolbar row */}
326
352
  {(folders.length > 1 ||
327
353
  (currentView === "folder-detail" && ToolbarExtras)) && (
328
354
  <Box
@@ -354,9 +380,19 @@ export default function TrackSelect({
354
380
  </Box>
355
381
  )}
356
382
 
357
- <Stack direction="row" spacing={2} sx={{ width: "100%" }}>
358
- {/* Left panel - swaps between FolderList and DataGrid */}
359
- <Box sx={{ flex: 3, minWidth: 0 }}>
383
+ <Stack
384
+ direction={{ xs: "column", md: "row" }}
385
+ spacing={2}
386
+ sx={{ width: "100%" }}
387
+ >
388
+ {/* Left panel */}
389
+ <Box
390
+ sx={{
391
+ flex: { xs: "none", md: 3 },
392
+ minWidth: 0,
393
+ width: { xs: "100%", md: "auto" },
394
+ }}
395
+ >
360
396
  {currentView === "folder-list" ? (
361
397
  <FolderList
362
398
  folders={folders}
@@ -375,8 +411,14 @@ export default function TrackSelect({
375
411
  />
376
412
  )}
377
413
  </Box>
378
- {/* Right panel - always visible */}
379
- <Box sx={{ flex: 2, minWidth: 0 }}>
414
+ {/* Right panel */}
415
+ <Box
416
+ sx={{
417
+ flex: { xs: "none", md: 2 },
418
+ minWidth: 0,
419
+ width: { xs: "100%", md: "auto" },
420
+ }}
421
+ >
380
422
  <TreeViewWrapper
381
423
  folderTrees={folderTrees}
382
424
  selectedCount={selectedCount}
@@ -392,13 +434,24 @@ export default function TrackSelect({
392
434
  gap: 2,
393
435
  }}
394
436
  >
395
- <Button
396
- variant="outlined"
397
- color="secondary"
398
- onClick={handleClear}
399
- >
400
- Clear
401
- </Button>
437
+ <Box sx={{ display: "flex", gap: 2 }}>
438
+ <Button
439
+ variant="outlined"
440
+ color="secondary"
441
+ onClick={handleClear}
442
+ >
443
+ Clear
444
+ </Button>
445
+ {initialSelection && (
446
+ <Button
447
+ variant="outlined"
448
+ color="secondary"
449
+ onClick={handleReset}
450
+ >
451
+ Reset to Default
452
+ </Button>
453
+ )}
454
+ </Box>
402
455
  <Box sx={{ display: "flex", gap: 2 }}>
403
456
  <Button variant="outlined" onClick={handleCancel}>
404
457
  Cancel
@@ -412,68 +465,23 @@ export default function TrackSelect({
412
465
  </Button>
413
466
  </Box>
414
467
  </Box>
415
- <Dialog
468
+ <LimitDialog
416
469
  open={limitDialogOpen}
417
470
  onClose={() => setLimitDialogOpen(false)}
418
- >
419
- <DialogTitle>Track Limit Reached</DialogTitle>
420
- <DialogContent>
421
- <DialogContentText>
422
- You can select up to {maxTracksLimit} tracks at a time. Please
423
- remove a track before adding another.
424
- </DialogContentText>
425
- </DialogContent>
426
- <DialogActions>
427
- <Button onClick={() => setLimitDialogOpen(false)} autoFocus>
428
- OK
429
- </Button>
430
- </DialogActions>
431
- </Dialog>
432
- <Dialog
471
+ maxTracks={maxTracksLimit}
472
+ />
473
+ <ClearDialog
433
474
  open={clearDialogOpen}
434
475
  onClose={() => setClearDialogOpen(false)}
435
- >
436
- <DialogTitle
437
- sx={{
438
- bgcolor: "#0c184a",
439
- color: "white",
440
- fontWeight: "bold",
441
- }}
442
- >
443
- {currentView === "folder-detail"
444
- ? `Clear ${activeFolder.label}`
445
- : "Clear All Folders"}
446
- </DialogTitle>
447
- <DialogContent sx={{ mt: 2 }}>
448
- <DialogContentText>
449
- {currentView === "folder-detail" ? (
450
- <>
451
- Are you sure you want to clear the selection for{" "}
452
- <strong>{activeFolder.label}</strong>?
453
- </>
454
- ) : (
455
- "Are you sure you want to clear all selections?"
456
- )}
457
- </DialogContentText>
458
- </DialogContent>
459
- <DialogActions sx={{ justifyContent: "center", gap: 2, pb: 2 }}>
460
- <Button
461
- variant="contained"
462
- color="primary"
463
- onClick={() => setClearDialogOpen(false)}
464
- autoFocus
465
- >
466
- Cancel
467
- </Button>
468
- <Button
469
- variant="outlined"
470
- color="secondary"
471
- onClick={confirmClear}
472
- >
473
- Clear
474
- </Button>
475
- </DialogActions>
476
- </Dialog>
476
+ onConfirm={confirmClear}
477
+ folderLabel={activeFolder.label}
478
+ clearAll={currentView === "folder-list"}
479
+ />
480
+ <ResetDialog
481
+ open={resetDialogOpen}
482
+ onClose={() => setResetDialogOpen(false)}
483
+ onConfirm={confirmReset}
484
+ />
477
485
  </Box>
478
486
  )}
479
487
  </DialogContent>
@@ -1,214 +1,214 @@
1
- import Folder from "@mui/icons-material/Folder";
2
- import IndeterminateCheckBoxRoundedIcon from "@mui/icons-material/IndeterminateCheckBoxRounded";
3
- import { Box, Stack, Tooltip, Typography } from "@mui/material";
4
- import Collapse from "@mui/material/Collapse";
5
- import { alpha, styled } from "@mui/material/styles";
6
- import {
7
- TreeItemCheckbox,
8
- TreeItemIconContainer,
9
- TreeItemLabel,
10
- } from "@mui/x-tree-view/TreeItem";
11
- import { TreeItemIcon } from "@mui/x-tree-view/TreeItemIcon";
12
- import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
13
- import { useTreeItemModel } from "@mui/x-tree-view/hooks";
14
- import { useTreeItem } from "@mui/x-tree-view/useTreeItem";
15
- import React, { ReactNode } from "react";
16
- import {
17
- CustomLabelProps,
18
- CustomTreeItemProps,
19
- ExtendedTreeItemProps,
20
- } from "../types";
21
-
22
- // Everything below is styling for the custom directory look of the tree view
23
- const TreeItemRoot = styled("li")(({ theme }) => ({
24
- listStyle: "none",
25
- margin: 0,
26
- padding: 0,
27
- outline: 4,
28
- color: theme.palette.grey[400],
29
- ...theme.applyStyles("light", {
30
- color: theme.palette.grey[600], // controls colors of the MUI icons
31
- }),
32
- }));
33
-
34
- const TreeItemLabelText = styled(Typography)({
35
- color: "black",
36
- fontFamily: "inherit",
37
- overflow: "hidden",
38
- textOverflow: "ellipsis",
39
- whiteSpace: "nowrap",
40
- });
41
-
42
- function CustomLabel({
43
- icon: Icon,
44
- children,
45
- isAssayItem,
46
- assayName,
47
- renderIcon,
48
- ...other
49
- }: CustomLabelProps) {
50
- const variant = "body2";
51
- const fontWeight = 500;
52
- const labelText = typeof children === "string" ? children : "";
53
- return (
54
- <TreeItemLabel
55
- {...other}
56
- sx={{
57
- display: "flex",
58
- alignItems: "center",
59
- minWidth: 0,
60
- overflow: "hidden",
61
- flex: 1,
62
- }}
63
- >
64
- {Icon && React.isValidElement(Icon) ? (
65
- <Box className="labelIcon" sx={{ mr: 1, flexShrink: 0 }}>
66
- {Icon}
67
- </Box>
68
- ) : (
69
- <Box
70
- component={Icon as React.ElementType}
71
- className="labelIcon"
72
- color="inherit"
73
- sx={{ mr: 1, fontSize: "1.2rem", flexShrink: 0 }}
74
- />
75
- )}
76
- <Stack
77
- direction="row"
78
- spacing={1}
79
- alignItems="center"
80
- sx={{ minWidth: 0, overflow: "hidden", flex: 1 }}
81
- >
82
- {assayName && renderIcon && (
83
- <Box sx={{ flexShrink: 0 }}>{renderIcon(assayName)}</Box>
84
- )}
85
- <Tooltip title={labelText} enterDelay={500} placement="top">
86
- <TreeItemLabelText fontWeight={fontWeight} variant={variant}>
87
- {labelText}
88
- </TreeItemLabelText>
89
- </Tooltip>
90
- </Stack>
91
- </TreeItemLabel>
92
- );
93
- }
94
-
95
- const TreeItemContent = styled("div")(({ theme }) => ({
96
- padding: theme.spacing(0.5),
97
- paddingRight: theme.spacing(2),
98
- paddingLeft: `calc(${theme.spacing(1)} + var(--TreeView-itemChildrenIndentation) * var(--TreeView-itemDepth))`,
99
- width: "100%",
100
- boxSizing: "border-box", // prevent width + padding to overflow
101
- position: "relative",
102
- display: "flex",
103
- alignItems: "center",
104
- gap: theme.spacing(1),
105
- cursor: "pointer",
106
- WebkitTapHighlightColor: "transparent",
107
- flexDirection: "row-reverse",
108
- borderRadius: theme.spacing(0.7),
109
- marginBottom: theme.spacing(0.5),
110
- marginTop: theme.spacing(0.5),
111
- fontWeight: 500,
112
- "&:hover": {
113
- backgroundColor: alpha(theme.palette.primary.main, 0.1),
114
- color: "white",
115
- ...theme.applyStyles("light", {
116
- color: theme.palette.primary.main,
117
- }),
118
- },
119
- }));
120
-
121
- const getIconFromTreeItemType = (
122
- itemType: string,
123
- renderIcon?: (name: string) => ReactNode,
124
- ) => {
125
- switch (itemType) {
126
- case "folder":
127
- return Folder;
128
- case "removeable":
129
- return IndeterminateCheckBoxRoundedIcon;
130
- default:
131
- return renderIcon ? renderIcon(itemType) : Folder;
132
- }
133
- };
134
-
135
- export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
136
- props: CustomTreeItemProps,
137
- ref: React.Ref<HTMLLIElement>,
138
- ) {
139
- const {
140
- id,
141
- itemId,
142
- label,
143
- disabled,
144
- children,
145
- onRemove,
146
- renderIcon,
147
- ...other
148
- } = props;
149
-
150
- const {
151
- getContextProviderProps,
152
- getRootProps,
153
- getContentProps,
154
- getIconContainerProps,
155
- getCheckboxProps,
156
- getLabelProps,
157
- getGroupTransitionProps,
158
- status,
159
- } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
160
-
161
- const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
162
- const icon = getIconFromTreeItemType(item.icon, renderIcon);
163
-
164
- const handleRemoveIconClick = (e: React.MouseEvent) => {
165
- e.stopPropagation(); // prevent item expand/select
166
- onRemove?.(item);
167
- };
168
-
169
- return (
170
- <TreeItemProvider {...getContextProviderProps()}>
171
- <TreeItemRoot {...getRootProps(other)}>
172
- <TreeItemContent {...getContentProps()}>
173
- <TreeItemIconContainer {...getIconContainerProps()}>
174
- <TreeItemIcon status={status} />
175
- </TreeItemIconContainer>
176
- <TreeItemCheckbox {...getCheckboxProps()} />
177
- <CustomLabel
178
- {...getLabelProps({
179
- icon:
180
- item.icon === "removeable" ? (
181
- <Box
182
- onClick={handleRemoveIconClick}
183
- sx={{
184
- width: 20,
185
- height: 20,
186
- display: "flex",
187
- alignItems: "center",
188
- justifyContent: "center",
189
- borderRadius: "4px",
190
- cursor: "pointer",
191
- mr: 1,
192
- "&:hover": {
193
- backgroundColor: "rgba(0,0,0,0.1)",
194
- },
195
- }}
196
- >
197
- <IndeterminateCheckBoxRoundedIcon fontSize="small" />
198
- </Box>
199
- ) : (
200
- icon
201
- ),
202
- expandable: (status.expandable && status.expanded).toString(),
203
- isAssayItem: item.isAssayItem,
204
- assayName: item.assayName,
205
- id: item.id,
206
- renderIcon,
207
- })}
208
- />
209
- </TreeItemContent>
210
- {children && <Collapse {...getGroupTransitionProps()} />}
211
- </TreeItemRoot>
212
- </TreeItemProvider>
213
- );
214
- });
1
+ import Folder from "@mui/icons-material/Folder";
2
+ import IndeterminateCheckBoxRoundedIcon from "@mui/icons-material/IndeterminateCheckBoxRounded";
3
+ import { Box, Stack, Tooltip, Typography } from "@mui/material";
4
+ import Collapse from "@mui/material/Collapse";
5
+ import { alpha, styled } from "@mui/material/styles";
6
+ import {
7
+ TreeItemCheckbox,
8
+ TreeItemIconContainer,
9
+ TreeItemLabel,
10
+ } from "@mui/x-tree-view/TreeItem";
11
+ import { TreeItemIcon } from "@mui/x-tree-view/TreeItemIcon";
12
+ import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
13
+ import { useTreeItemModel } from "@mui/x-tree-view/hooks";
14
+ import { useTreeItem } from "@mui/x-tree-view/useTreeItem";
15
+ import React, { ReactNode } from "react";
16
+ import {
17
+ CustomLabelProps,
18
+ CustomTreeItemProps,
19
+ ExtendedTreeItemProps,
20
+ } from "../types";
21
+
22
+ // Everything below is styling for the custom directory look of the tree view
23
+ const TreeItemRoot = styled("li")(({ theme }) => ({
24
+ listStyle: "none",
25
+ margin: 0,
26
+ padding: 0,
27
+ outline: 4,
28
+ color: theme.palette.grey[400],
29
+ ...theme.applyStyles("light", {
30
+ color: theme.palette.grey[600], // controls colors of the MUI icons
31
+ }),
32
+ }));
33
+
34
+ const TreeItemLabelText = styled(Typography)({
35
+ color: "black",
36
+ fontFamily: "inherit",
37
+ overflow: "hidden",
38
+ textOverflow: "ellipsis",
39
+ whiteSpace: "nowrap",
40
+ });
41
+
42
+ function CustomLabel({
43
+ icon: Icon,
44
+ children,
45
+ isAssayItem,
46
+ assayName,
47
+ renderIcon,
48
+ ...other
49
+ }: CustomLabelProps) {
50
+ const variant = "body2";
51
+ const fontWeight = 500;
52
+ const labelText = typeof children === "string" ? children : "";
53
+ return (
54
+ <TreeItemLabel
55
+ {...other}
56
+ sx={{
57
+ display: "flex",
58
+ alignItems: "center",
59
+ minWidth: 0,
60
+ overflow: "hidden",
61
+ flex: 1,
62
+ }}
63
+ >
64
+ {Icon && React.isValidElement(Icon) ? (
65
+ <Box className="labelIcon" sx={{ mr: 1, flexShrink: 0 }}>
66
+ {Icon}
67
+ </Box>
68
+ ) : (
69
+ <Box
70
+ component={Icon as React.ElementType}
71
+ className="labelIcon"
72
+ color="inherit"
73
+ sx={{ mr: 1, fontSize: "1.2rem", flexShrink: 0 }}
74
+ />
75
+ )}
76
+ <Stack
77
+ direction="row"
78
+ spacing={1}
79
+ alignItems="center"
80
+ sx={{ minWidth: 0, overflow: "hidden", flex: 1 }}
81
+ >
82
+ {assayName && renderIcon && (
83
+ <Box sx={{ flexShrink: 0 }}>{renderIcon(assayName)}</Box>
84
+ )}
85
+ <Tooltip title={labelText} enterDelay={500} placement="top">
86
+ <TreeItemLabelText fontWeight={fontWeight} variant={variant}>
87
+ {labelText}
88
+ </TreeItemLabelText>
89
+ </Tooltip>
90
+ </Stack>
91
+ </TreeItemLabel>
92
+ );
93
+ }
94
+
95
+ const TreeItemContent = styled("div")(({ theme }) => ({
96
+ padding: theme.spacing(0.5),
97
+ paddingRight: theme.spacing(2),
98
+ paddingLeft: `calc(${theme.spacing(1)} + var(--TreeView-itemChildrenIndentation) * var(--TreeView-itemDepth))`,
99
+ width: "100%",
100
+ boxSizing: "border-box", // prevent width + padding to overflow
101
+ position: "relative",
102
+ display: "flex",
103
+ alignItems: "center",
104
+ gap: theme.spacing(1),
105
+ cursor: "pointer",
106
+ WebkitTapHighlightColor: "transparent",
107
+ flexDirection: "row-reverse",
108
+ borderRadius: theme.spacing(0.7),
109
+ marginBottom: theme.spacing(0.5),
110
+ marginTop: theme.spacing(0.5),
111
+ fontWeight: 500,
112
+ "&:hover": {
113
+ backgroundColor: alpha(theme.palette.primary.main, 0.1),
114
+ color: "white",
115
+ ...theme.applyStyles("light", {
116
+ color: theme.palette.primary.main,
117
+ }),
118
+ },
119
+ }));
120
+
121
+ const getIconFromTreeItemType = (
122
+ itemType: string,
123
+ renderIcon?: (name: string) => ReactNode,
124
+ ) => {
125
+ switch (itemType) {
126
+ case "folder":
127
+ return Folder;
128
+ case "removeable":
129
+ return IndeterminateCheckBoxRoundedIcon;
130
+ default:
131
+ return renderIcon ? renderIcon(itemType) : Folder;
132
+ }
133
+ };
134
+
135
+ export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
136
+ props: CustomTreeItemProps,
137
+ ref: React.Ref<HTMLLIElement>,
138
+ ) {
139
+ const {
140
+ id,
141
+ itemId,
142
+ label,
143
+ disabled,
144
+ children,
145
+ onRemove,
146
+ renderIcon,
147
+ ...other
148
+ } = props;
149
+
150
+ const {
151
+ getContextProviderProps,
152
+ getRootProps,
153
+ getContentProps,
154
+ getIconContainerProps,
155
+ getCheckboxProps,
156
+ getLabelProps,
157
+ getGroupTransitionProps,
158
+ status,
159
+ } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
160
+
161
+ const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
162
+ const icon = getIconFromTreeItemType(item.icon, renderIcon);
163
+
164
+ const handleRemoveIconClick = (e: React.MouseEvent) => {
165
+ e.stopPropagation(); // prevent item expand/select
166
+ onRemove?.(item);
167
+ };
168
+
169
+ return (
170
+ <TreeItemProvider {...getContextProviderProps()}>
171
+ <TreeItemRoot {...getRootProps(other)}>
172
+ <TreeItemContent {...getContentProps()}>
173
+ <TreeItemIconContainer {...getIconContainerProps()}>
174
+ <TreeItemIcon status={status} />
175
+ </TreeItemIconContainer>
176
+ <TreeItemCheckbox {...getCheckboxProps()} />
177
+ <CustomLabel
178
+ {...getLabelProps({
179
+ icon:
180
+ item.icon === "removeable" ? (
181
+ <Box
182
+ onClick={handleRemoveIconClick}
183
+ sx={{
184
+ width: 20,
185
+ height: 20,
186
+ display: "flex",
187
+ alignItems: "center",
188
+ justifyContent: "center",
189
+ borderRadius: "4px",
190
+ cursor: "pointer",
191
+ mr: 1,
192
+ "&:hover": {
193
+ backgroundColor: "rgba(0,0,0,0.1)",
194
+ },
195
+ }}
196
+ >
197
+ <IndeterminateCheckBoxRoundedIcon fontSize="small" />
198
+ </Box>
199
+ ) : (
200
+ icon
201
+ ),
202
+ expandable: (status.expandable && status.expanded).toString(),
203
+ isAssayItem: item.isAssayItem,
204
+ assayName: item.assayName,
205
+ id: item.id,
206
+ renderIcon,
207
+ })}
208
+ />
209
+ </TreeItemContent>
210
+ {children && <Collapse {...getGroupTransitionProps()} />}
211
+ </TreeItemRoot>
212
+ </TreeItemProvider>
213
+ );
214
+ });