@weng-lab/genomebrowser-ui 0.3.6 → 0.4.0-beta.1

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 (91) hide show
  1. package/.env.local +1 -1
  2. package/dist/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.d.ts +7 -0
  3. package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +1 -13
  4. package/dist/TrackSelect/Folders/biosamples/shared/toTrack.d.ts +20 -0
  5. package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +4 -13
  6. package/dist/TrackSelect/Folders/genes/shared/columns.d.ts +2 -2
  7. package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +1 -3
  8. package/dist/TrackSelect/Folders/genes/shared/toTrack.d.ts +18 -0
  9. package/dist/TrackSelect/Folders/genes/shared/types.d.ts +2 -0
  10. package/dist/TrackSelect/Folders/index.d.ts +6 -12
  11. package/dist/TrackSelect/Folders/mohd/data/human.json.d.ts +2948 -0
  12. package/dist/TrackSelect/Folders/mohd/human.d.ts +1 -0
  13. package/dist/TrackSelect/Folders/mohd/shared/MohdGroupingCell.d.ts +2 -0
  14. package/dist/TrackSelect/Folders/mohd/shared/MohdTreeItem.d.ts +3 -0
  15. package/dist/TrackSelect/Folders/mohd/shared/MohdViewSelector.d.ts +7 -0
  16. package/dist/TrackSelect/Folders/mohd/shared/columns.d.ts +5 -0
  17. package/dist/TrackSelect/Folders/mohd/shared/config.d.ts +42 -0
  18. package/dist/TrackSelect/Folders/mohd/shared/createFolder.d.ts +9 -0
  19. package/dist/TrackSelect/Folders/mohd/shared/toTrack.d.ts +9 -0
  20. package/dist/TrackSelect/Folders/mohd/shared/types.d.ts +40 -0
  21. package/dist/TrackSelect/Folders/other-tracks/shared/toTrack.d.ts +5 -0
  22. package/dist/TrackSelect/Folders/other-tracks/shared/types.d.ts +1 -0
  23. package/dist/TrackSelect/Folders/types.d.ts +23 -55
  24. package/dist/TrackSelect/TrackSelect.d.ts +10 -7
  25. package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
  26. package/dist/TrackSelect/buildSelectedTree.d.ts +15 -0
  27. package/dist/TrackSelect/managedTracks.d.ts +13 -0
  28. package/dist/TrackSelect/resolveFolderView.d.ts +2 -0
  29. package/dist/TrackSelect/trackContext.d.ts +5 -0
  30. package/dist/TrackSelect/types.d.ts +12 -33
  31. package/dist/genomebrowser-ui.es.js +2231 -1732
  32. package/dist/genomebrowser-ui.es.js.map +1 -1
  33. package/dist/lib.d.ts +4 -4
  34. package/dist/muiLicense.d.ts +1 -0
  35. package/package.json +6 -3
  36. package/src/TrackSelect/Dialogs/ClearDialog.tsx +3 -8
  37. package/src/TrackSelect/Dialogs/ResetDialog.tsx +5 -4
  38. package/src/TrackSelect/FolderList/FolderCard.tsx +1 -1
  39. package/src/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.tsx +33 -0
  40. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +39 -58
  41. package/src/TrackSelect/Folders/biosamples/shared/toTrack.ts +138 -0
  42. package/src/TrackSelect/Folders/biosamples/shared/types.ts +4 -16
  43. package/src/TrackSelect/Folders/genes/shared/columns.tsx +2 -2
  44. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +11 -31
  45. package/src/TrackSelect/Folders/genes/shared/toTrack.ts +59 -0
  46. package/src/TrackSelect/Folders/genes/shared/types.ts +2 -0
  47. package/src/TrackSelect/Folders/index.ts +14 -17
  48. package/src/TrackSelect/Folders/mohd/data/human.json +2945 -0
  49. package/src/TrackSelect/Folders/mohd/human.ts +10 -0
  50. package/src/TrackSelect/Folders/mohd/shared/MohdGroupingCell.tsx +68 -0
  51. package/src/TrackSelect/Folders/mohd/shared/MohdTreeItem.tsx +17 -0
  52. package/src/TrackSelect/Folders/mohd/shared/MohdViewSelector.tsx +33 -0
  53. package/src/TrackSelect/Folders/mohd/shared/columns.tsx +79 -0
  54. package/src/TrackSelect/Folders/mohd/shared/config.tsx +71 -0
  55. package/src/TrackSelect/Folders/mohd/shared/createFolder.ts +144 -0
  56. package/src/TrackSelect/Folders/mohd/shared/toTrack.ts +164 -0
  57. package/src/TrackSelect/Folders/mohd/shared/types.ts +46 -0
  58. package/src/TrackSelect/Folders/other-tracks/shared/createFolder.ts +13 -14
  59. package/src/TrackSelect/Folders/other-tracks/shared/toTrack.ts +17 -0
  60. package/src/TrackSelect/Folders/other-tracks/shared/types.ts +1 -0
  61. package/src/TrackSelect/Folders/types.ts +26 -69
  62. package/src/TrackSelect/TrackSelect.tsx +301 -257
  63. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +9 -9
  64. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +84 -6
  65. package/src/TrackSelect/buildSelectedTree.ts +145 -0
  66. package/src/TrackSelect/managedTracks.ts +92 -0
  67. package/src/TrackSelect/resolveFolderView.ts +20 -0
  68. package/src/TrackSelect/trackContext.ts +9 -0
  69. package/src/TrackSelect/types.ts +14 -39
  70. package/src/lib.ts +13 -7
  71. package/src/muiLicense.ts +9 -0
  72. package/src/vite-env.d.ts +9 -0
  73. package/test/TrackSelect.test.tsx +435 -0
  74. package/test/main.tsx +36 -352
  75. package/test/mocks/logo-test.tsx +11 -0
  76. package/test/mohdDisplay.test.tsx +45 -0
  77. package/test/startup.test.ts +206 -0
  78. package/test/trackSelectState.test.ts +176 -0
  79. package/vite.config.ts +1 -0
  80. package/vitest.config.ts +20 -0
  81. package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +0 -18
  82. package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +0 -28
  83. package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +0 -13
  84. package/dist/TrackSelect/Folders/other-tracks/shared/treeBuilder.d.ts +0 -4
  85. package/dist/TrackSelect/store.d.ts +0 -4
  86. package/src/TrackSelect/Folders/NEW.md +0 -929
  87. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +0 -78
  88. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +0 -224
  89. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +0 -45
  90. package/src/TrackSelect/Folders/other-tracks/shared/treeBuilder.ts +0 -34
  91. package/src/TrackSelect/store.ts +0 -117
@@ -1,3 +1,5 @@
1
+ import { TreeViewBaseItem } from "@mui/x-tree-view";
2
+ import { TrackStoreInstance } from "@weng-lab/genomebrowser";
1
3
  import CloseIcon from "@mui/icons-material/Close";
2
4
  import {
3
5
  Box,
@@ -8,28 +10,33 @@ import {
8
10
  IconButton,
9
11
  Stack,
10
12
  } from "@mui/material";
11
- import { TreeViewBaseItem } from "@mui/x-tree-view";
12
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13
+ import { useEffect, useState } from "react";
14
+ import { Assembly, FolderDefinition } from "./Folders/types";
13
15
  import { DataGridWrapper } from "./DataGrid/DataGridWrapper";
14
16
  import { ClearDialog } from "./Dialogs/ClearDialog";
15
17
  import { LimitDialog } from "./Dialogs/LimitDialog";
16
18
  import { ResetDialog } from "./Dialogs/ResetDialog";
17
19
  import { Breadcrumb } from "./FolderList/Breadcrumb";
18
20
  import { FolderList } from "./FolderList/FolderList";
19
- import { FolderDefinition, FolderRuntimeConfig } from "./Folders/types";
20
- import { createSelectionStore, SelectionStoreInstance } from "./store";
21
- import { TreeViewWrapper } from "./TreeView/TreeViewWrapper";
21
+ import { diffManagedTracks } from "./managedTracks";
22
+ import { resolveFolderView } from "./resolveFolderView";
23
+ import type { TrackSelectTrackContext } from "./trackContext";
22
24
  import { ExtendedTreeItemProps } from "./types";
25
+ import { TreeViewWrapper } from "./TreeView/TreeViewWrapper";
26
+
27
+ export type InitialSelectedIdsByAssembly = Partial<
28
+ Record<Assembly, Record<string, string[]>>
29
+ >;
23
30
 
24
31
  export interface TrackSelectProps {
32
+ assembly: Assembly;
25
33
  folders: FolderDefinition[];
26
- onSubmit: (selectedByFolder: Map<string, Set<string>>) => void;
34
+ initialSelectedIds?: InitialSelectedIdsByAssembly;
35
+ sessionStorageKey?: string;
36
+ trackStore?: TrackStoreInstance;
27
37
  onCancel?: () => void;
28
- onClear?: () => void;
29
38
  maxTracks?: number;
30
- storageKey?: string;
31
- /** Initial selection to use when no stored selection exists */
32
- initialSelection?: Map<string, Set<string>>;
39
+ trackContext?: TrackSelectTrackContext;
33
40
  open: boolean;
34
41
  onClose: () => void;
35
42
  title?: string;
@@ -37,51 +44,113 @@ export interface TrackSelectProps {
37
44
 
38
45
  const DEFAULT_MAX_TRACKS = 30;
39
46
 
47
+ const DEFAULT_TITLE = "Track Select";
48
+
40
49
  type ViewState = "folder-list" | "folder-detail";
41
50
 
42
- const cloneSelectionMap = (selection: Map<string, Set<string>>) => {
43
- const map = new Map<string, Set<string>>();
44
- selection.forEach((ids, folderId) => {
45
- map.set(folderId, new Set(ids));
46
- });
47
- return map;
51
+ const createEmptySelectedByFolder = (folders: FolderDefinition[]) => {
52
+ return new Map(folders.map((folder) => [folder.id, new Set<string>()]));
53
+ };
54
+
55
+ const cloneSelectedByFolder = (selectedByFolder: Map<string, Set<string>>) => {
56
+ return new Map(
57
+ Array.from(selectedByFolder, ([folderId, ids]) => [folderId, new Set(ids)]),
58
+ );
48
59
  };
49
60
 
50
- const buildRuntimeConfigMap = (folders: FolderDefinition[]) => {
51
- const map = new Map<string, FolderRuntimeConfig>();
61
+ const normalizeSelectedByFolder = ({
62
+ folders,
63
+ selectedIdsByFolder,
64
+ }: {
65
+ folders: FolderDefinition[];
66
+ selectedIdsByFolder?: Record<string, string[]>;
67
+ }) => {
68
+ const normalized = createEmptySelectedByFolder(folders);
69
+
70
+ if (!selectedIdsByFolder) {
71
+ return normalized;
72
+ }
73
+
52
74
  folders.forEach((folder) => {
53
- map.set(folder.id, {
54
- columns: folder.columns,
55
- groupingModel: folder.groupingModel,
56
- leafField: folder.leafField,
57
- });
75
+ const ids = selectedIdsByFolder[folder.id];
76
+ if (!ids) {
77
+ return;
78
+ }
79
+
80
+ normalized.set(
81
+ folder.id,
82
+ new Set(ids.filter((id) => id.startsWith(`${folder.id}/`))),
83
+ );
58
84
  });
59
- return map;
85
+
86
+ return normalized;
60
87
  };
61
88
 
62
- const attachFolderId = (
63
- items: TreeViewBaseItem<ExtendedTreeItemProps>[],
64
- folderId: string,
65
- ): TreeViewBaseItem<ExtendedTreeItemProps>[] => {
66
- return items.map((item) => ({
67
- ...item,
68
- folderId,
69
- children: item.children
70
- ? attachFolderId(item.children, folderId)
71
- : undefined,
72
- }));
89
+ const loadSelectedByFolder = ({
90
+ assembly,
91
+ folders,
92
+ initialSelectedIds,
93
+ sessionStorageKey,
94
+ }: {
95
+ assembly: Assembly;
96
+ folders: FolderDefinition[];
97
+ initialSelectedIds?: InitialSelectedIdsByAssembly;
98
+ sessionStorageKey?: string;
99
+ }) => {
100
+ const fallback = normalizeSelectedByFolder({
101
+ folders,
102
+ selectedIdsByFolder: initialSelectedIds?.[assembly],
103
+ });
104
+
105
+ if (!sessionStorageKey || typeof window === "undefined") {
106
+ return fallback;
107
+ }
108
+
109
+ const storedValue = window.sessionStorage.getItem(sessionStorageKey);
110
+ if (!storedValue) {
111
+ return fallback;
112
+ }
113
+
114
+ try {
115
+ const parsed = JSON.parse(storedValue) as Record<string, string[]>;
116
+ return normalizeSelectedByFolder({
117
+ folders,
118
+ selectedIdsByFolder: parsed,
119
+ });
120
+ } catch {
121
+ return fallback;
122
+ }
73
123
  };
74
124
 
75
- const DEFAULT_TITLE = "Track Select";
125
+ const saveSelectedByFolder = ({
126
+ selectedByFolder,
127
+ sessionStorageKey,
128
+ }: {
129
+ selectedByFolder: Map<string, Set<string>>;
130
+ sessionStorageKey?: string;
131
+ }) => {
132
+ if (!sessionStorageKey || typeof window === "undefined") {
133
+ return;
134
+ }
135
+
136
+ const serialized = Object.fromEntries(
137
+ Array.from(selectedByFolder, ([folderId, ids]) => [
138
+ folderId,
139
+ Array.from(ids),
140
+ ]),
141
+ );
142
+ window.sessionStorage.setItem(sessionStorageKey, JSON.stringify(serialized));
143
+ };
76
144
 
77
145
  export default function TrackSelect({
146
+ assembly,
78
147
  folders,
79
- onSubmit,
148
+ initialSelectedIds,
149
+ sessionStorageKey,
150
+ trackStore,
80
151
  onCancel,
81
- onClear,
82
152
  maxTracks,
83
- storageKey,
84
- initialSelection,
153
+ trackContext,
85
154
  open,
86
155
  onClose,
87
156
  title = DEFAULT_TITLE,
@@ -89,127 +158,122 @@ export default function TrackSelect({
89
158
  const [limitDialogOpen, setLimitDialogOpen] = useState(false);
90
159
  const [clearDialogOpen, setClearDialogOpen] = useState(false);
91
160
  const [resetDialogOpen, setResetDialogOpen] = useState(false);
92
- const [runtimeConfigByFolder, setRuntimeConfigByFolder] = useState(() =>
93
- buildRuntimeConfigMap(folders),
94
- );
95
-
96
- // View state: folder list or folder detail
97
161
  const [currentView, setCurrentView] = useState<ViewState>(() =>
98
162
  folders.length > 1 ? "folder-list" : "folder-detail",
99
163
  );
100
-
101
- // Create and memoize the selection store
102
- const folderIds = useMemo(() => folders.map((f) => f.id), [folders]);
103
- const storeRef = useRef<SelectionStoreInstance | null>(null);
104
- if (!storeRef.current) {
105
- storeRef.current = createSelectionStore(
106
- folderIds,
107
- storageKey,
108
- initialSelection,
109
- );
110
- }
111
- const store = storeRef.current;
112
-
113
- // Subscribe to store changes
114
- const selectedByFolder = store((state) => state.selectedByFolder);
115
- const activeFolderId = store((state) => state.activeFolderId);
116
- const setActiveFolder = store((state) => state.setActiveFolder);
117
- const setSelection = store((state) => state.setSelection);
118
- const clear = store((state) => state.clear);
119
-
120
- // Keep a committed snapshot for cancel functionality
121
- const [committedSnapshot, setCommittedSnapshot] = useState(() =>
122
- cloneSelectionMap(selectedByFolder),
164
+ const [activeFolderId, setActiveFolderId] = useState(
165
+ () => folders[0]?.id ?? "",
166
+ );
167
+ const [activeViewIdByFolder, setActiveViewIdByFolder] = useState(
168
+ () =>
169
+ new Map(
170
+ folders.flatMap((folder) =>
171
+ folder.views?.[0] ? [[folder.id, folder.views[0].id] as const] : [],
172
+ ),
173
+ ),
174
+ );
175
+ const [committedSelectedByFolder, setCommittedSelectedByFolder] = useState(
176
+ () =>
177
+ loadSelectedByFolder({
178
+ assembly,
179
+ folders,
180
+ initialSelectedIds,
181
+ sessionStorageKey,
182
+ }),
123
183
  );
184
+ const [selectedByFolder, setSelectedByFolder] = useState(() =>
185
+ cloneSelectedByFolder(committedSelectedByFolder),
186
+ );
187
+ const maxTracksLimit = maxTracks ?? DEFAULT_MAX_TRACKS;
124
188
 
125
189
  useEffect(() => {
126
- setRuntimeConfigByFolder(buildRuntimeConfigMap(folders));
127
- // Ensure active folder is valid
128
- if (!folders.some((folder) => folder.id === activeFolderId)) {
129
- setActiveFolder(folders[0]?.id ?? "");
130
- }
131
- // Update view state if folder count changes
190
+ setActiveFolderId((currentFolderId) => {
191
+ if (folders.some((folder) => folder.id === currentFolderId)) {
192
+ return currentFolderId;
193
+ }
194
+
195
+ return folders[0]?.id ?? "";
196
+ });
197
+
132
198
  if (folders.length <= 1) {
133
199
  setCurrentView("folder-detail");
134
200
  }
135
- }, [folders, activeFolderId, setActiveFolder]);
136
-
137
- const activeFolder = useMemo(() => {
138
- return folders.find((folder) => folder.id === activeFolderId) ?? folders[0];
139
- }, [folders, activeFolderId]);
140
-
141
- const activeConfig = useMemo(() => {
142
- if (!activeFolder) return undefined;
143
- return (
144
- runtimeConfigByFolder.get(activeFolder.id) ?? {
145
- columns: activeFolder.columns,
146
- groupingModel: activeFolder.groupingModel,
147
- leafField: activeFolder.leafField,
148
- }
149
- );
150
- }, [runtimeConfigByFolder, activeFolder]);
151
201
 
152
- const selectedIds = useMemo(() => {
153
- if (!activeFolder) return new Set<string>();
154
- return new Set(selectedByFolder.get(activeFolder.id) ?? []);
155
- }, [selectedByFolder, activeFolder]);
202
+ setActiveViewIdByFolder((current) => {
203
+ return new Map(
204
+ folders.flatMap((folder) => {
205
+ if (!folder.views?.length) {
206
+ return [];
207
+ }
208
+
209
+ const activeViewId = current.get(folder.id);
210
+ if (
211
+ activeViewId &&
212
+ folder.views.some((view) => view.id === activeViewId)
213
+ ) {
214
+ return [[folder.id, activeViewId] as const];
215
+ }
216
+
217
+ return [[folder.id, folder.views[0].id] as const];
218
+ }),
219
+ );
220
+ });
221
+ }, [folders]);
156
222
 
157
- const selectedCount = useMemo(() => {
158
- let total = 0;
159
- selectedByFolder.forEach((ids) => {
160
- total += ids.size;
223
+ useEffect(() => {
224
+ const nextCommittedSelection = loadSelectedByFolder({
225
+ assembly,
226
+ folders,
227
+ initialSelectedIds,
228
+ sessionStorageKey,
161
229
  });
162
- return total;
163
- }, [selectedByFolder]);
230
+ setCommittedSelectedByFolder(nextCommittedSelection);
231
+ setSelectedByFolder(cloneSelectedByFolder(nextCommittedSelection));
232
+ }, [assembly, folders, initialSelectedIds, sessionStorageKey]);
164
233
 
165
- const maxTracksLimit = maxTracks ?? DEFAULT_MAX_TRACKS;
234
+ useEffect(() => {
235
+ if (!open) {
236
+ return;
237
+ }
166
238
 
167
- const rows = useMemo(() => {
168
- if (!activeFolder) return [];
169
- return Array.from(activeFolder.rowById.values());
170
- }, [activeFolder]);
171
-
172
- const folderTrees = useMemo(() => {
173
- return folders
174
- .filter((folder) => {
175
- const selected = selectedByFolder.get(folder.id);
176
- return selected && selected.size > 0;
177
- })
178
- .map((folder) => {
179
- const config = runtimeConfigByFolder.get(folder.id);
180
- const buildTree = config?.buildTree ?? folder.buildTree;
181
-
182
- return {
183
- folderId: folder.id,
184
- items: attachFolderId(
185
- buildTree(
186
- Array.from(selectedByFolder.get(folder.id) ?? []),
187
- folder.rowById,
188
- ),
189
- folder.id,
190
- ),
191
- TreeItemComponent: folder.TreeItemComponent,
192
- };
193
- });
194
- }, [folders, selectedByFolder, runtimeConfigByFolder]);
195
-
196
- const updateActiveFolderConfig = useCallback(
197
- (partial: Partial<FolderRuntimeConfig>) => {
198
- if (!activeFolder) return;
199
- setRuntimeConfigByFolder((prev) => {
200
- const current = prev.get(activeFolder.id);
201
- if (!current) return prev;
202
- const next = new Map(prev);
203
- next.set(activeFolder.id, { ...current, ...partial });
204
- return next;
205
- });
206
- },
207
- [activeFolder],
239
+ setSelectedByFolder(cloneSelectedByFolder(committedSelectedByFolder));
240
+ }, [committedSelectedByFolder, open]);
241
+
242
+ useEffect(() => {
243
+ if (!assembly || !trackStore) {
244
+ return;
245
+ }
246
+
247
+ const { idsToRemove, tracksToAdd } = diffManagedTracks({
248
+ assembly,
249
+ currentTracks: trackStore.getState().tracks,
250
+ folders,
251
+ selectedByFolder: committedSelectedByFolder,
252
+ trackContext,
253
+ });
254
+
255
+ const { insertTrack, removeTrack } = trackStore.getState();
256
+ idsToRemove.forEach((id) => removeTrack(id));
257
+ tracksToAdd.forEach((track) => insertTrack(track));
258
+ }, [assembly, committedSelectedByFolder, folders, trackContext, trackStore]);
259
+
260
+ const activeFolder =
261
+ folders.find((folder) => folder.id === activeFolderId) ?? folders[0];
262
+ const activeConfig = activeFolder
263
+ ? resolveFolderView(activeFolder, activeViewIdByFolder)
264
+ : undefined;
265
+ const activeViewId = activeConfig?.id ?? "";
266
+ const rows = activeFolder?.rows ?? [];
267
+ const selectedIds = new Set(
268
+ selectedByFolder.get(activeFolder?.id ?? "") ?? [],
208
269
  );
270
+ let selectedCount = 0;
271
+ selectedByFolder.forEach((ids) => {
272
+ selectedCount += ids.size;
273
+ });
209
274
 
210
- // Navigation handlers
211
275
  const handleFolderSelect = (folderId: string) => {
212
- setActiveFolder(folderId);
276
+ setActiveFolderId(folderId);
213
277
  setCurrentView("folder-detail");
214
278
  };
215
279
 
@@ -217,15 +281,57 @@ export default function TrackSelect({
217
281
  setCurrentView("folder-list");
218
282
  };
219
283
 
284
+ const handleCancel = () => {
285
+ onCancel?.();
286
+ onClose();
287
+ };
288
+
289
+ const ViewSelector = activeFolder?.ViewSelector;
290
+
291
+ const confirmReset = () => {
292
+ setResetDialogOpen(false);
293
+ setSelectedByFolder(cloneSelectedByFolder(committedSelectedByFolder));
294
+ };
295
+
296
+ const confirmClear = () => {
297
+ setClearDialogOpen(false);
298
+
299
+ const nextSelectedByFolder = new Map(selectedByFolder);
300
+
301
+ if (currentView === "folder-detail") {
302
+ nextSelectedByFolder.set(activeFolderId, new Set<string>());
303
+ } else {
304
+ folders.forEach((folder) =>
305
+ nextSelectedByFolder.set(folder.id, new Set<string>()),
306
+ );
307
+ }
308
+
309
+ setSelectedByFolder(nextSelectedByFolder);
310
+ };
311
+
312
+ const handleActiveViewChange = (viewId: string) => {
313
+ if (!activeFolder) {
314
+ return;
315
+ }
316
+
317
+ setActiveViewIdByFolder((prev) => {
318
+ const next = new Map(prev);
319
+ next.set(activeFolder.id, viewId);
320
+ return next;
321
+ });
322
+ };
323
+
220
324
  const handleSelectionChange = (ids: Set<string>) => {
221
- if (!activeFolder) return;
325
+ if (!activeFolder) {
326
+ return;
327
+ }
222
328
 
223
- // Filter to only include IDs that exist in rowById (exclude auto-generated group IDs)
224
329
  const filteredIds = new Set(
225
- Array.from(ids).filter((id) => activeFolder.rowById.has(id)),
330
+ Array.from(ids).filter((id) =>
331
+ activeFolder.rows.some((row) => row.id === id),
332
+ ),
226
333
  );
227
334
 
228
- // Calculate what the total would be with this change
229
335
  let nextTotal = filteredIds.size;
230
336
  selectedByFolder.forEach((folderIds, folderId) => {
231
337
  if (folderId !== activeFolder.id) {
@@ -238,7 +344,9 @@ export default function TrackSelect({
238
344
  return;
239
345
  }
240
346
 
241
- setSelection(activeFolder.id, filteredIds);
347
+ const nextSelectedByFolder = new Map(selectedByFolder);
348
+ nextSelectedByFolder.set(activeFolder.id, filteredIds);
349
+ setSelectedByFolder(nextSelectedByFolder);
242
350
  };
243
351
 
244
352
  const handleRemoveTreeItem = (
@@ -249,85 +357,27 @@ export default function TrackSelect({
249
357
  return;
250
358
  }
251
359
 
252
- const current = selectedByFolder.get(folderId) ?? new Set<string>();
253
- const nextSet = new Set(current);
360
+ const nextSelectedByFolder = new Map(selectedByFolder);
361
+ const nextSet = new Set(
362
+ nextSelectedByFolder.get(folderId) ?? new Set<string>(),
363
+ );
254
364
  item.allExpAccessions.forEach((id) => nextSet.delete(id));
255
- setSelection(folderId, nextSet);
365
+ nextSelectedByFolder.set(folderId, nextSet);
366
+ setSelectedByFolder(nextSelectedByFolder);
256
367
  };
257
368
 
258
369
  const handleSubmit = () => {
259
- const committed = cloneSelectionMap(selectedByFolder);
260
- setCommittedSnapshot(committed);
261
- onSubmit(committed);
370
+ saveSelectedByFolder({ selectedByFolder, sessionStorageKey });
371
+ setCommittedSelectedByFolder(cloneSelectedByFolder(selectedByFolder));
262
372
  onClose();
263
373
  };
264
374
 
265
- const handleCancel = () => {
266
- // Restore from committed snapshot
267
- committedSnapshot.forEach((ids, folderId) => {
268
- setSelection(folderId, ids);
269
- });
270
- onCancel?.();
271
- onClose();
272
- };
273
-
274
- const handleClear = () => {
275
- setClearDialogOpen(true);
276
- };
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
-
302
- const confirmClear = () => {
303
- setClearDialogOpen(false);
304
- let newSnapshot: Map<string, Set<string>>;
305
-
306
- if (currentView === "folder-detail") {
307
- // Clear only the current folder
308
- clear(activeFolderId);
309
- newSnapshot = cloneSelectionMap(selectedByFolder);
310
- newSnapshot.set(activeFolderId, new Set<string>());
311
- } else {
312
- // Clear all folders
313
- clear();
314
- newSnapshot = new Map<string, Set<string>>();
315
- folderIds.forEach((id) => newSnapshot.set(id, new Set<string>()));
316
- onClear?.();
317
- }
318
-
319
- setCommittedSnapshot(newSnapshot);
320
- onSubmit(newSnapshot);
321
- };
322
-
323
- const ToolbarExtras = activeFolder?.ToolbarExtras;
324
-
325
375
  return (
326
376
  <Dialog open={open} onClose={handleCancel} maxWidth="lg" fullWidth>
327
377
  <DialogTitle
328
378
  sx={{
329
- bgcolor: "#0c184a",
330
- color: "white",
379
+ bgcolor: "primary.main",
380
+ color: "primary.contrastText",
331
381
  display: "flex",
332
382
  justifyContent: "space-between",
333
383
  alignItems: "center",
@@ -338,7 +388,7 @@ export default function TrackSelect({
338
388
  <IconButton
339
389
  size="large"
340
390
  onClick={handleCancel}
341
- sx={{ color: "white", padding: 0 }}
391
+ sx={{ color: "primary.contrastText", p: 0 }}
342
392
  >
343
393
  <CloseIcon fontSize="large" />
344
394
  </IconButton>
@@ -348,44 +398,37 @@ export default function TrackSelect({
348
398
  <Box sx={{ p: 2 }}>No folders available.</Box>
349
399
  ) : (
350
400
  <Box sx={{ flex: 1, pt: 1 }}>
351
- {/* Toolbar row */}
352
- {(folders.length > 1 ||
353
- (currentView === "folder-detail" && ToolbarExtras)) && (
354
- <Box
355
- display="flex"
356
- justifyContent="space-between"
357
- alignItems="center"
358
- sx={{ mb: 2 }}
359
- >
360
- {folders.length > 1 ? (
361
- <Breadcrumb
362
- currentFolder={
363
- currentView === "folder-detail" ? activeFolder : null
364
- }
365
- onNavigateToRoot={handleNavigateToRoot}
366
- />
367
- ) : (
368
- <Box />
369
- )}
370
- {currentView === "folder-detail" &&
371
- ToolbarExtras &&
372
- activeConfig && (
373
- <ToolbarExtras
374
- updateConfig={updateActiveFolderConfig}
375
- folderId={activeFolder.id}
376
- label={activeFolder.label}
377
- config={activeConfig}
378
- />
379
- )}
380
- </Box>
381
- )}
382
-
401
+ <Box
402
+ display="flex"
403
+ justifyContent="space-between"
404
+ alignItems="center"
405
+ sx={{ mb: 2 }}
406
+ >
407
+ {folders.length > 1 ? (
408
+ <Breadcrumb
409
+ currentFolder={
410
+ currentView === "folder-detail" ? activeFolder : null
411
+ }
412
+ onNavigateToRoot={handleNavigateToRoot}
413
+ />
414
+ ) : (
415
+ <Box />
416
+ )}
417
+ {currentView === "folder-detail" &&
418
+ ViewSelector &&
419
+ activeFolder.views ? (
420
+ <ViewSelector
421
+ views={activeFolder.views}
422
+ activeViewId={activeViewId}
423
+ onChange={handleActiveViewChange}
424
+ />
425
+ ) : null}
426
+ </Box>
383
427
  <Stack
384
428
  direction={{ xs: "column", md: "row" }}
385
429
  spacing={2}
386
430
  sx={{ width: "100%" }}
387
431
  >
388
- {/* Left panel - FolderList or DataGrid */}
389
432
  <Box
390
433
  sx={{
391
434
  flex: { xs: "none", md: 3 },
@@ -411,7 +454,6 @@ export default function TrackSelect({
411
454
  />
412
455
  )}
413
456
  </Box>
414
- {/* Right panel - Active Tracks */}
415
457
  <Box
416
458
  sx={{
417
459
  flex: { xs: "none", md: 2 },
@@ -420,7 +462,9 @@ export default function TrackSelect({
420
462
  }}
421
463
  >
422
464
  <TreeViewWrapper
423
- folderTrees={folderTrees}
465
+ folders={folders}
466
+ selectedByFolder={selectedByFolder}
467
+ activeViewIdByFolder={activeViewIdByFolder}
424
468
  selectedCount={selectedCount}
425
469
  onRemove={handleRemoveTreeItem}
426
470
  />
@@ -440,20 +484,20 @@ export default function TrackSelect({
440
484
  variant="outlined"
441
485
  color="secondary"
442
486
  size="small"
443
- onClick={handleClear}
487
+ onClick={() => setClearDialogOpen(true)}
444
488
  >
445
489
  Clear
446
490
  </Button>
447
- {initialSelection && (
491
+ {Boolean(trackStore) ? (
448
492
  <Button
449
493
  variant="outlined"
450
494
  color="secondary"
451
495
  size="small"
452
- onClick={handleReset}
496
+ onClick={() => setResetDialogOpen(true)}
453
497
  >
454
498
  Reset
455
499
  </Button>
456
- )}
500
+ ) : null}
457
501
  </Box>
458
502
  <Box sx={{ display: "flex", gap: 1 }}>
459
503
  <Button variant="outlined" size="small" onClick={handleCancel}>