@weng-lab/genomebrowser-ui 0.1.11 → 0.2.0-beta.0
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/.env.local +1 -0
- package/dist/TrackSelect/DataGrid/DefaultGroupingCell.d.ts +6 -0
- package/dist/TrackSelect/FolderList/Breadcrumb.d.ts +6 -0
- package/dist/TrackSelect/FolderList/FolderCard.d.ts +6 -0
- package/dist/TrackSelect/FolderList/FolderList.d.ts +6 -0
- package/dist/TrackSelect/{Data/humanBiosamples.json.d.ts → Folders/biosamples/data/human.json.d.ts} +1940 -1919
- package/dist/TrackSelect/{Data/mouseBiosamples.json.d.ts → Folders/biosamples/data/mouse.json.d.ts} +408 -357
- package/dist/TrackSelect/Folders/biosamples/human.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/mouse.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +14 -0
- package/dist/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.d.ts +6 -0
- package/dist/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/shared/columns.d.ts +14 -0
- package/dist/TrackSelect/Folders/biosamples/shared/constants.d.ts +19 -0
- package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +24 -0
- package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +25 -0
- package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +44 -0
- package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +10 -0
- package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +10 -0
- package/dist/TrackSelect/Folders/genes/human.d.ts +7 -0
- package/dist/TrackSelect/Folders/genes/mouse.d.ts +7 -0
- package/dist/TrackSelect/Folders/genes/shared/columns.d.ts +14 -0
- package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +12 -0
- package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +13 -0
- package/dist/TrackSelect/Folders/genes/shared/types.d.ts +26 -0
- package/dist/TrackSelect/Folders/index.d.ts +14 -0
- package/dist/TrackSelect/Folders/types.d.ts +76 -0
- package/dist/TrackSelect/TrackSelect.d.ts +12 -5
- package/dist/TrackSelect/TreeView/CustomTreeItem.d.ts +3 -0
- package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
- package/dist/TrackSelect/store.d.ts +1 -2
- package/dist/TrackSelect/types.d.ts +24 -62
- package/dist/genomebrowser-ui.es.js +1373 -2117
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/package.json +3 -3
- package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +36 -20
- package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -0
- package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -0
- package/src/TrackSelect/FolderList/FolderCard.tsx +51 -0
- package/src/TrackSelect/FolderList/FolderList.tsx +47 -0
- package/src/TrackSelect/Folders/NEW.md +929 -0
- package/src/TrackSelect/{Data → Folders/biosamples/data}/formatBiosamples.go +2 -2
- package/src/TrackSelect/{Data/humanBiosamples.json → Folders/biosamples/data/human.json} +1940 -1919
- package/src/TrackSelect/{Data/mouseBiosamples.json → Folders/biosamples/data/mouse.json} +408 -357
- package/src/TrackSelect/Folders/biosamples/human.ts +17 -0
- package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -0
- package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +65 -0
- package/src/TrackSelect/{DataGrid/GroupingCell.tsx → Folders/biosamples/shared/BiosampleGroupingCell.tsx} +7 -5
- package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -0
- package/src/TrackSelect/{DataGrid → Folders/biosamples/shared}/columns.tsx +31 -17
- package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -0
- package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -0
- package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +227 -0
- package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -0
- package/src/TrackSelect/Folders/genes/data/human.json +7 -0
- package/src/TrackSelect/Folders/genes/data/mouse.json +7 -0
- package/src/TrackSelect/Folders/genes/human.ts +16 -0
- package/src/TrackSelect/Folders/genes/mouse.ts +16 -0
- package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -0
- package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -0
- package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -0
- package/src/TrackSelect/Folders/genes/shared/types.ts +29 -0
- package/src/TrackSelect/Folders/index.ts +27 -0
- package/src/TrackSelect/Folders/types.ts +95 -0
- package/src/TrackSelect/TrackSelect.tsx +409 -311
- package/src/TrackSelect/TreeView/CustomTreeItem.tsx +217 -0
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +47 -42
- package/src/TrackSelect/store.ts +103 -46
- package/src/TrackSelect/types.ts +28 -74
- package/src/lib.ts +2 -2
- package/test/main.tsx +113 -169
- package/.claude/settings.local.json +0 -7
- package/dist/TrackSelect/DataGrid/CustomToolbar.d.ts +0 -12
- package/dist/TrackSelect/DataGrid/GroupingCell.d.ts +0 -2
- package/dist/TrackSelect/DataGrid/columns.d.ts +0 -4
- package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +0 -49
- package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +0 -49
- package/dist/TrackSelect/consts.d.ts +0 -11
- package/src/TrackSelect/DataGrid/CustomToolbar.tsx +0 -152
- package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +0 -155
- package/src/TrackSelect/TreeView/treeViewHelpers.tsx +0 -475
- package/src/TrackSelect/consts.ts +0 -92
- package/src/TrackSelect/issues.md +0 -404
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
1
2
|
import {
|
|
2
3
|
Box,
|
|
3
4
|
Button,
|
|
@@ -6,367 +7,464 @@ import {
|
|
|
6
7
|
DialogContent,
|
|
7
8
|
DialogContentText,
|
|
8
9
|
DialogTitle,
|
|
9
|
-
|
|
10
|
+
IconButton,
|
|
10
11
|
Stack,
|
|
11
|
-
Switch,
|
|
12
|
-
TextField,
|
|
13
12
|
} from "@mui/material";
|
|
14
|
-
import { GridRowSelectionModel } from "@mui/x-data-grid";
|
|
15
13
|
import { TreeViewBaseItem } from "@mui/x-tree-view";
|
|
16
|
-
import
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
import { Breadcrumb } from "./FolderList/Breadcrumb";
|
|
17
16
|
import { DataGridWrapper } from "./DataGrid/DataGridWrapper";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
getTracksData,
|
|
22
|
-
} from "./DataGrid/dataGridHelpers";
|
|
17
|
+
import { FolderList } from "./FolderList/FolderList";
|
|
18
|
+
import { FolderDefinition, FolderRuntimeConfig } from "./Folders/types";
|
|
19
|
+
import { createSelectionStore, SelectionStoreInstance } from "./store";
|
|
23
20
|
import { TreeViewWrapper } from "./TreeView/TreeViewWrapper";
|
|
24
|
-
import {
|
|
25
|
-
buildSortedAssayTreeView,
|
|
26
|
-
buildTreeView,
|
|
27
|
-
searchTreeItems,
|
|
28
|
-
} from "./TreeView/treeViewHelpers";
|
|
29
|
-
import { SelectionStoreInstance } from "./store";
|
|
30
|
-
import { ExtendedTreeItemProps, SearchTracksProps } from "./types";
|
|
21
|
+
import { ExtendedTreeItemProps } from "./types";
|
|
31
22
|
|
|
32
23
|
export interface TrackSelectProps {
|
|
33
|
-
|
|
34
|
-
onSubmit
|
|
24
|
+
folders: FolderDefinition[];
|
|
25
|
+
onSubmit: (selectedByFolder: Map<string, Set<string>>) => void;
|
|
35
26
|
onCancel?: () => void;
|
|
36
|
-
|
|
27
|
+
onClear?: () => void;
|
|
28
|
+
maxTracks?: number;
|
|
29
|
+
storageKey?: string;
|
|
30
|
+
/** Initial selection to use when no stored selection exists */
|
|
31
|
+
initialSelection?: Map<string, Set<string>>;
|
|
32
|
+
open: boolean;
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
title?: string;
|
|
37
35
|
}
|
|
38
36
|
|
|
37
|
+
const DEFAULT_MAX_TRACKS = 30;
|
|
38
|
+
|
|
39
|
+
type ViewState = "folder-list" | "folder-detail";
|
|
40
|
+
|
|
41
|
+
const cloneSelectionMap = (selection: Map<string, Set<string>>) => {
|
|
42
|
+
const map = new Map<string, Set<string>>();
|
|
43
|
+
selection.forEach((ids, folderId) => {
|
|
44
|
+
map.set(folderId, new Set(ids));
|
|
45
|
+
});
|
|
46
|
+
return map;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const buildRuntimeConfigMap = (folders: FolderDefinition[]) => {
|
|
50
|
+
const map = new Map<string, FolderRuntimeConfig>();
|
|
51
|
+
folders.forEach((folder) => {
|
|
52
|
+
map.set(folder.id, {
|
|
53
|
+
columns: folder.columns,
|
|
54
|
+
groupingModel: folder.groupingModel,
|
|
55
|
+
leafField: folder.leafField,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return map;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const attachFolderId = (
|
|
62
|
+
items: TreeViewBaseItem<ExtendedTreeItemProps>[],
|
|
63
|
+
folderId: string,
|
|
64
|
+
): TreeViewBaseItem<ExtendedTreeItemProps>[] => {
|
|
65
|
+
return items.map((item) => ({
|
|
66
|
+
...item,
|
|
67
|
+
folderId,
|
|
68
|
+
children: item.children
|
|
69
|
+
? attachFolderId(item.children, folderId)
|
|
70
|
+
: undefined,
|
|
71
|
+
}));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DEFAULT_TITLE = "Track Select";
|
|
75
|
+
|
|
39
76
|
export default function TrackSelect({
|
|
40
|
-
|
|
77
|
+
folders,
|
|
41
78
|
onSubmit,
|
|
42
79
|
onCancel,
|
|
43
|
-
|
|
80
|
+
onClear,
|
|
81
|
+
maxTracks,
|
|
82
|
+
storageKey,
|
|
83
|
+
initialSelection,
|
|
84
|
+
open,
|
|
85
|
+
onClose,
|
|
86
|
+
title = DEFAULT_TITLE,
|
|
44
87
|
}: TrackSelectProps) {
|
|
45
88
|
const [limitDialogOpen, setLimitDialogOpen] = useState(false);
|
|
46
|
-
const [
|
|
47
|
-
const [
|
|
48
|
-
|
|
49
|
-
const selectedIds = store((s) => s.selectedIds);
|
|
50
|
-
const setSelected = store((s) => s.setSelected);
|
|
51
|
-
const clear = store((s) => s.clear);
|
|
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),
|
|
89
|
+
const [clearDialogOpen, setClearDialogOpen] = useState(false);
|
|
90
|
+
const [runtimeConfigByFolder, setRuntimeConfigByFolder] = useState(() =>
|
|
91
|
+
buildRuntimeConfigMap(folders),
|
|
60
92
|
);
|
|
61
93
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
[assembly],
|
|
94
|
+
// View state: folder list or folder detail
|
|
95
|
+
const [currentView, setCurrentView] = useState<ViewState>(() =>
|
|
96
|
+
folders.length > 1 ? "folder-list" : "folder-detail",
|
|
66
97
|
);
|
|
67
98
|
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
99
|
+
// Create and memoize the selection store
|
|
100
|
+
const folderIds = useMemo(() => folders.map((f) => f.id), [folders]);
|
|
101
|
+
const storeRef = useRef<SelectionStoreInstance | null>(null);
|
|
102
|
+
if (!storeRef.current) {
|
|
103
|
+
storeRef.current = createSelectionStore(
|
|
104
|
+
folderIds,
|
|
105
|
+
storageKey,
|
|
106
|
+
initialSelection,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const store = storeRef.current;
|
|
72
110
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
// Subscribe to store changes
|
|
112
|
+
const selectedByFolder = store((state) => state.selectedByFolder);
|
|
113
|
+
const activeFolderId = store((state) => state.activeFolderId);
|
|
114
|
+
const setActiveFolder = store((state) => state.setActiveFolder);
|
|
115
|
+
const setSelection = store((state) => state.setSelection);
|
|
116
|
+
const clear = store((state) => state.clear);
|
|
77
117
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
id: "1",
|
|
84
|
-
isAssayItem: false,
|
|
85
|
-
label: "Biosamples",
|
|
86
|
-
icon: "folder",
|
|
87
|
-
children: [],
|
|
88
|
-
allRowInfo: [],
|
|
89
|
-
},
|
|
90
|
-
rowById,
|
|
91
|
-
)
|
|
92
|
-
: buildTreeView(
|
|
93
|
-
Array.from(workingTrackIds),
|
|
94
|
-
{
|
|
95
|
-
id: "1",
|
|
96
|
-
isAssayItem: false,
|
|
97
|
-
label: "Biosamples",
|
|
98
|
-
icon: "folder",
|
|
99
|
-
children: [],
|
|
100
|
-
allRowInfo: [],
|
|
101
|
-
},
|
|
102
|
-
rowById,
|
|
103
|
-
);
|
|
104
|
-
}, [workingTrackIds, sortedAssay, rowById]);
|
|
105
|
-
|
|
106
|
-
const [filteredRows, setFilteredRows] = useState(rows);
|
|
107
|
-
const [filteredTreeItems, setFilteredTreeItems] = useState([
|
|
108
|
-
{
|
|
109
|
-
id: "1",
|
|
110
|
-
isAssayItem: false,
|
|
111
|
-
label: "Biosamples",
|
|
112
|
-
icon: "folder",
|
|
113
|
-
children: [],
|
|
114
|
-
allRowInfo: [],
|
|
115
|
-
},
|
|
116
|
-
] as TreeViewBaseItem<ExtendedTreeItemProps>[]);
|
|
117
|
-
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
118
|
-
const searchResultIdsRef = useRef<Set<string>>(new Set());
|
|
118
|
+
// Keep a committed snapshot for cancel functionality
|
|
119
|
+
const [committedSnapshot, setCommittedSnapshot] = useState(() =>
|
|
120
|
+
cloneSelectionMap(selectedByFolder),
|
|
121
|
+
);
|
|
119
122
|
|
|
120
123
|
useEffect(() => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
searchResultIdsRef.current = new Set();
|
|
126
|
-
} else if (searchResultIdsRef.current.size > 0) {
|
|
127
|
-
// When selection changes during search, rebuild tree from selected items that match search
|
|
128
|
-
const matchingTrackIds = Array.from(workingTrackIds).filter((id) =>
|
|
129
|
-
searchResultIdsRef.current.has(id),
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const newTreeItems = sortedAssay
|
|
133
|
-
? buildSortedAssayTreeView(
|
|
134
|
-
matchingTrackIds,
|
|
135
|
-
{
|
|
136
|
-
id: "1",
|
|
137
|
-
isAssayItem: false,
|
|
138
|
-
label: "Biosamples",
|
|
139
|
-
icon: "folder",
|
|
140
|
-
children: [],
|
|
141
|
-
allRowInfo: [],
|
|
142
|
-
},
|
|
143
|
-
rowById,
|
|
144
|
-
)
|
|
145
|
-
: buildTreeView(
|
|
146
|
-
matchingTrackIds,
|
|
147
|
-
{
|
|
148
|
-
id: "1",
|
|
149
|
-
isAssayItem: false,
|
|
150
|
-
label: "Biosamples",
|
|
151
|
-
icon: "folder",
|
|
152
|
-
children: [],
|
|
153
|
-
allRowInfo: [],
|
|
154
|
-
},
|
|
155
|
-
rowById,
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
setFilteredTreeItems(newTreeItems);
|
|
124
|
+
setRuntimeConfigByFolder(buildRuntimeConfigMap(folders));
|
|
125
|
+
// Ensure active folder is valid
|
|
126
|
+
if (!folders.some((folder) => folder.id === activeFolderId)) {
|
|
127
|
+
setActiveFolder(folders[0]?.id ?? "");
|
|
159
128
|
}
|
|
160
|
-
|
|
129
|
+
// Update view state if folder count changes
|
|
130
|
+
if (folders.length <= 1) {
|
|
131
|
+
setCurrentView("folder-detail");
|
|
132
|
+
}
|
|
133
|
+
}, [folders, activeFolderId, setActiveFolder]);
|
|
161
134
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
};
|
|
135
|
+
const activeFolder = useMemo(() => {
|
|
136
|
+
return folders.find((folder) => folder.id === activeFolderId) ?? folders[0];
|
|
137
|
+
}, [folders, activeFolderId]);
|
|
138
|
+
|
|
139
|
+
const activeConfig = useMemo(() => {
|
|
140
|
+
if (!activeFolder) return undefined;
|
|
141
|
+
return (
|
|
142
|
+
runtimeConfigByFolder.get(activeFolder.id) ?? {
|
|
143
|
+
columns: activeFolder.columns,
|
|
144
|
+
groupingModel: activeFolder.groupingModel,
|
|
145
|
+
leafField: activeFolder.leafField,
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}, [runtimeConfigByFolder, activeFolder]);
|
|
165
149
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
150
|
+
const selectedIds = useMemo(() => {
|
|
151
|
+
if (!activeFolder) return new Set<string>();
|
|
152
|
+
return new Set(selectedByFolder.get(activeFolder.id) ?? []);
|
|
153
|
+
}, [selectedByFolder, activeFolder]);
|
|
169
154
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
155
|
+
const selectedCount = useMemo(() => {
|
|
156
|
+
let total = 0;
|
|
157
|
+
selectedByFolder.forEach((ids) => {
|
|
158
|
+
total += ids.size;
|
|
159
|
+
});
|
|
160
|
+
return total;
|
|
161
|
+
}, [selectedByFolder]);
|
|
174
162
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
163
|
+
const maxTracksLimit = maxTracks ?? DEFAULT_MAX_TRACKS;
|
|
164
|
+
|
|
165
|
+
const rows = useMemo(() => {
|
|
166
|
+
if (!activeFolder) return [];
|
|
167
|
+
return Array.from(activeFolder.rowById.values());
|
|
168
|
+
}, [activeFolder]);
|
|
180
169
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
newTreeIds,
|
|
222
|
-
{
|
|
223
|
-
id: "1",
|
|
224
|
-
isAssayItem: false,
|
|
225
|
-
label: "Biosamples",
|
|
226
|
-
icon: "folder",
|
|
227
|
-
children: [],
|
|
228
|
-
allRowInfo: [],
|
|
229
|
-
},
|
|
230
|
-
rowById,
|
|
231
|
-
)
|
|
232
|
-
: buildTreeView(
|
|
233
|
-
newTreeIds,
|
|
234
|
-
{
|
|
235
|
-
id: "1",
|
|
236
|
-
isAssayItem: false,
|
|
237
|
-
label: "Biosamples",
|
|
238
|
-
icon: "folder",
|
|
239
|
-
children: [],
|
|
240
|
-
allRowInfo: [],
|
|
241
|
-
},
|
|
242
|
-
rowById,
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
// Store search result IDs in ref for use in useEffect
|
|
246
|
-
searchResultIdsRef.current = new Set(newDataGridIds);
|
|
247
|
-
|
|
248
|
-
setFilteredRows(newDataGridRows);
|
|
249
|
-
setIsSearchResult(true);
|
|
250
|
-
setFilteredTreeItems(newTreeItems);
|
|
251
|
-
}, 300);
|
|
170
|
+
const treeItems = useMemo(() => {
|
|
171
|
+
if (!activeFolder) return [];
|
|
172
|
+
return folders
|
|
173
|
+
.filter((folder) => {
|
|
174
|
+
const selected = selectedByFolder.get(folder.id);
|
|
175
|
+
return selected && selected.size > 0;
|
|
176
|
+
})
|
|
177
|
+
.flatMap((folder) =>
|
|
178
|
+
attachFolderId(
|
|
179
|
+
folder.buildTree(
|
|
180
|
+
Array.from(selectedByFolder.get(folder.id) ?? []),
|
|
181
|
+
folder.rowById,
|
|
182
|
+
),
|
|
183
|
+
folder.id,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}, [folders, selectedByFolder, activeFolder]);
|
|
187
|
+
|
|
188
|
+
const updateActiveFolderConfig = useCallback(
|
|
189
|
+
(partial: Partial<FolderRuntimeConfig>) => {
|
|
190
|
+
if (!activeFolder) return;
|
|
191
|
+
setRuntimeConfigByFolder((prev) => {
|
|
192
|
+
const current = prev.get(activeFolder.id);
|
|
193
|
+
if (!current) return prev;
|
|
194
|
+
const next = new Map(prev);
|
|
195
|
+
next.set(activeFolder.id, { ...current, ...partial });
|
|
196
|
+
return next;
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
[activeFolder],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Navigation handlers
|
|
203
|
+
const handleFolderSelect = (folderId: string) => {
|
|
204
|
+
setActiveFolder(folderId);
|
|
205
|
+
setCurrentView("folder-detail");
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handleNavigateToRoot = () => {
|
|
209
|
+
setCurrentView("folder-list");
|
|
252
210
|
};
|
|
253
211
|
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
212
|
+
const handleSelectionChange = (ids: Set<string>) => {
|
|
213
|
+
if (!activeFolder) return;
|
|
214
|
+
|
|
215
|
+
// Filter to only include IDs that exist in rowById (exclude auto-generated group IDs)
|
|
216
|
+
const filteredIds = new Set(
|
|
217
|
+
Array.from(ids).filter((id) => activeFolder.rowById.has(id)),
|
|
218
|
+
);
|
|
257
219
|
|
|
258
|
-
//
|
|
259
|
-
let
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
220
|
+
// Calculate what the total would be with this change
|
|
221
|
+
let nextTotal = filteredIds.size;
|
|
222
|
+
selectedByFolder.forEach((folderIds, folderId) => {
|
|
223
|
+
if (folderId !== activeFolder.id) {
|
|
224
|
+
nextTotal += folderIds.size;
|
|
263
225
|
}
|
|
264
226
|
});
|
|
265
227
|
|
|
266
|
-
|
|
267
|
-
if (realTrackCount > MAX_ACTIVE) {
|
|
228
|
+
if (nextTotal > maxTracksLimit) {
|
|
268
229
|
setLimitDialogOpen(true);
|
|
269
230
|
return;
|
|
270
231
|
}
|
|
271
232
|
|
|
272
|
-
|
|
273
|
-
|
|
233
|
+
setSelection(activeFolder.id, filteredIds);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleRemoveTreeItem = (
|
|
237
|
+
item: TreeViewBaseItem<ExtendedTreeItemProps>,
|
|
238
|
+
) => {
|
|
239
|
+
const folderId = item.folderId;
|
|
240
|
+
if (!folderId || !item.allExpAccessions?.length) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const current = selectedByFolder.get(folderId) ?? new Set<string>();
|
|
245
|
+
const nextSet = new Set(current);
|
|
246
|
+
item.allExpAccessions.forEach((id) => nextSet.delete(id));
|
|
247
|
+
setSelection(folderId, nextSet);
|
|
274
248
|
};
|
|
275
249
|
|
|
276
250
|
const handleSubmit = () => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
251
|
+
const committed = cloneSelectionMap(selectedByFolder);
|
|
252
|
+
setCommittedSnapshot(committed);
|
|
253
|
+
onSubmit(committed);
|
|
254
|
+
onClose();
|
|
281
255
|
};
|
|
282
256
|
|
|
283
257
|
const handleCancel = () => {
|
|
284
|
-
//
|
|
285
|
-
|
|
258
|
+
// Restore from committed snapshot
|
|
259
|
+
committedSnapshot.forEach((ids, folderId) => {
|
|
260
|
+
setSelection(folderId, ids);
|
|
261
|
+
});
|
|
286
262
|
onCancel?.();
|
|
263
|
+
onClose();
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const handleClear = () => {
|
|
267
|
+
setClearDialogOpen(true);
|
|
287
268
|
};
|
|
288
269
|
|
|
270
|
+
const confirmClear = () => {
|
|
271
|
+
setClearDialogOpen(false);
|
|
272
|
+
let newSnapshot: Map<string, Set<string>>;
|
|
273
|
+
|
|
274
|
+
if (currentView === "folder-detail") {
|
|
275
|
+
// Clear only the current folder
|
|
276
|
+
clear(activeFolderId);
|
|
277
|
+
newSnapshot = cloneSelectionMap(selectedByFolder);
|
|
278
|
+
newSnapshot.set(activeFolderId, new Set<string>());
|
|
279
|
+
} else {
|
|
280
|
+
// Clear all folders
|
|
281
|
+
clear();
|
|
282
|
+
newSnapshot = new Map<string, Set<string>>();
|
|
283
|
+
folderIds.forEach((id) => newSnapshot.set(id, new Set<string>()));
|
|
284
|
+
onClear?.();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
setCommittedSnapshot(newSnapshot);
|
|
288
|
+
onSubmit(newSnapshot);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const ToolbarExtras = activeFolder?.ToolbarExtras;
|
|
292
|
+
|
|
289
293
|
return (
|
|
290
|
-
<
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}
|
|
301
|
-
value="Sort by assay"
|
|
302
|
-
control={<Switch color="primary" onChange={handleToggle} />}
|
|
303
|
-
label="Sort by assay"
|
|
304
|
-
labelPlacement="end"
|
|
305
|
-
/>
|
|
306
|
-
</Box>
|
|
307
|
-
<Stack direction="row" spacing={2} sx={{ width: "100%" }}>
|
|
308
|
-
<Box sx={{ flex: 3, minWidth: 0 }}>
|
|
309
|
-
<DataGridWrapper
|
|
310
|
-
rows={filteredRows}
|
|
311
|
-
label={
|
|
312
|
-
isSearchResult
|
|
313
|
-
? `${filteredRows.length} Search Results`
|
|
314
|
-
: `${rows.length} Available Tracks`
|
|
315
|
-
}
|
|
316
|
-
selectedIds={workingIds}
|
|
317
|
-
handleSelection={handleSelection}
|
|
318
|
-
sortedAssay={sortedAssay}
|
|
319
|
-
/>
|
|
320
|
-
</Box>
|
|
321
|
-
<Box sx={{ flex: 2, minWidth: 0 }}>
|
|
322
|
-
<TreeViewWrapper
|
|
323
|
-
store={store}
|
|
324
|
-
items={filteredTreeItems}
|
|
325
|
-
trackIds={workingTrackIds}
|
|
326
|
-
isSearchResult={isSearchResult}
|
|
327
|
-
/>
|
|
328
|
-
</Box>
|
|
329
|
-
</Stack>
|
|
330
|
-
<Box
|
|
331
|
-
sx={{ display: "flex", justifyContent: "space-between", mt: 2, gap: 2 }}
|
|
294
|
+
<Dialog open={open} onClose={handleCancel} maxWidth="lg" fullWidth>
|
|
295
|
+
<DialogTitle
|
|
296
|
+
sx={{
|
|
297
|
+
bgcolor: "#0c184a",
|
|
298
|
+
color: "white",
|
|
299
|
+
display: "flex",
|
|
300
|
+
justifyContent: "space-between",
|
|
301
|
+
alignItems: "center",
|
|
302
|
+
fontWeight: "bold",
|
|
303
|
+
}}
|
|
332
304
|
>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
onClick={
|
|
337
|
-
|
|
338
|
-
onReset();
|
|
339
|
-
} else {
|
|
340
|
-
clear();
|
|
341
|
-
setWorkingIds(new Set());
|
|
342
|
-
}
|
|
343
|
-
}}
|
|
305
|
+
{title}
|
|
306
|
+
<IconButton
|
|
307
|
+
size="large"
|
|
308
|
+
onClick={handleCancel}
|
|
309
|
+
sx={{ color: "white", padding: 0 }}
|
|
344
310
|
>
|
|
345
|
-
|
|
346
|
-
</
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
311
|
+
<CloseIcon fontSize="large" />
|
|
312
|
+
</IconButton>
|
|
313
|
+
</DialogTitle>
|
|
314
|
+
<DialogContent sx={{ marginTop: "5px" }}>
|
|
315
|
+
{!activeFolder || !activeConfig ? (
|
|
316
|
+
<Box sx={{ p: 2 }}>No folders available.</Box>
|
|
317
|
+
) : (
|
|
318
|
+
<Box sx={{ flex: 1, pt: 1 }}>
|
|
319
|
+
{/* Toolbar row - breadcrumb on left, extras on right */}
|
|
320
|
+
{(folders.length > 1 ||
|
|
321
|
+
(currentView === "folder-detail" && ToolbarExtras)) && (
|
|
322
|
+
<Box
|
|
323
|
+
display="flex"
|
|
324
|
+
justifyContent="space-between"
|
|
325
|
+
alignItems="center"
|
|
326
|
+
sx={{ mb: 2 }}
|
|
327
|
+
>
|
|
328
|
+
{folders.length > 1 ? (
|
|
329
|
+
<Breadcrumb
|
|
330
|
+
currentFolder={
|
|
331
|
+
currentView === "folder-detail" ? activeFolder : null
|
|
332
|
+
}
|
|
333
|
+
onNavigateToRoot={handleNavigateToRoot}
|
|
334
|
+
/>
|
|
335
|
+
) : (
|
|
336
|
+
<Box />
|
|
337
|
+
)}
|
|
338
|
+
{currentView === "folder-detail" && ToolbarExtras && (
|
|
339
|
+
<ToolbarExtras updateConfig={updateActiveFolderConfig} />
|
|
340
|
+
)}
|
|
341
|
+
</Box>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
<Stack direction="row" spacing={2} sx={{ width: "100%" }}>
|
|
345
|
+
{/* Left panel - swaps between FolderList and DataGrid */}
|
|
346
|
+
<Box sx={{ flex: 3, minWidth: 0 }}>
|
|
347
|
+
{currentView === "folder-list" ? (
|
|
348
|
+
<FolderList
|
|
349
|
+
folders={folders}
|
|
350
|
+
onFolderSelect={handleFolderSelect}
|
|
351
|
+
/>
|
|
352
|
+
) : (
|
|
353
|
+
<DataGridWrapper
|
|
354
|
+
rows={rows}
|
|
355
|
+
columns={activeConfig.columns}
|
|
356
|
+
groupingModel={activeConfig.groupingModel}
|
|
357
|
+
leafField={activeConfig.leafField}
|
|
358
|
+
label={`${rows.length} Available ${activeFolder.label}`}
|
|
359
|
+
selectedIds={selectedIds}
|
|
360
|
+
onSelectionChange={handleSelectionChange}
|
|
361
|
+
GroupingCellComponent={activeFolder.GroupingCellComponent}
|
|
362
|
+
/>
|
|
363
|
+
)}
|
|
364
|
+
</Box>
|
|
365
|
+
{/* Right panel - always visible */}
|
|
366
|
+
<Box sx={{ flex: 2, minWidth: 0 }}>
|
|
367
|
+
<TreeViewWrapper
|
|
368
|
+
items={treeItems}
|
|
369
|
+
selectedCount={selectedCount}
|
|
370
|
+
onRemove={handleRemoveTreeItem}
|
|
371
|
+
TreeItemComponent={activeFolder.TreeItemComponent}
|
|
372
|
+
/>
|
|
373
|
+
</Box>
|
|
374
|
+
</Stack>
|
|
375
|
+
<Box
|
|
376
|
+
sx={{
|
|
377
|
+
display: "flex",
|
|
378
|
+
justifyContent: "space-between",
|
|
379
|
+
mt: 2,
|
|
380
|
+
gap: 2,
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<Button
|
|
384
|
+
variant="outlined"
|
|
385
|
+
color="secondary"
|
|
386
|
+
onClick={handleClear}
|
|
387
|
+
>
|
|
388
|
+
Clear
|
|
389
|
+
</Button>
|
|
390
|
+
<Box sx={{ display: "flex", gap: 2 }}>
|
|
391
|
+
<Button variant="outlined" onClick={handleCancel}>
|
|
392
|
+
Cancel
|
|
393
|
+
</Button>
|
|
394
|
+
<Button
|
|
395
|
+
variant="contained"
|
|
396
|
+
color="primary"
|
|
397
|
+
onClick={handleSubmit}
|
|
398
|
+
>
|
|
399
|
+
Submit
|
|
400
|
+
</Button>
|
|
401
|
+
</Box>
|
|
402
|
+
</Box>
|
|
403
|
+
<Dialog
|
|
404
|
+
open={limitDialogOpen}
|
|
405
|
+
onClose={() => setLimitDialogOpen(false)}
|
|
406
|
+
>
|
|
407
|
+
<DialogTitle>Track Limit Reached</DialogTitle>
|
|
408
|
+
<DialogContent>
|
|
409
|
+
<DialogContentText>
|
|
410
|
+
You can select up to {maxTracksLimit} tracks at a time. Please
|
|
411
|
+
remove a track before adding another.
|
|
412
|
+
</DialogContentText>
|
|
413
|
+
</DialogContent>
|
|
414
|
+
<DialogActions>
|
|
415
|
+
<Button onClick={() => setLimitDialogOpen(false)} autoFocus>
|
|
416
|
+
OK
|
|
417
|
+
</Button>
|
|
418
|
+
</DialogActions>
|
|
419
|
+
</Dialog>
|
|
420
|
+
<Dialog
|
|
421
|
+
open={clearDialogOpen}
|
|
422
|
+
onClose={() => setClearDialogOpen(false)}
|
|
423
|
+
>
|
|
424
|
+
<DialogTitle
|
|
425
|
+
sx={{
|
|
426
|
+
bgcolor: "#0c184a",
|
|
427
|
+
color: "white",
|
|
428
|
+
fontWeight: "bold",
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
{currentView === "folder-detail"
|
|
432
|
+
? `Clear ${activeFolder.label}`
|
|
433
|
+
: "Clear All Folders"}
|
|
434
|
+
</DialogTitle>
|
|
435
|
+
<DialogContent sx={{ mt: 2 }}>
|
|
436
|
+
<DialogContentText>
|
|
437
|
+
{currentView === "folder-detail" ? (
|
|
438
|
+
<>
|
|
439
|
+
Are you sure you want to clear the selection for{" "}
|
|
440
|
+
<strong>{activeFolder.label}</strong>?
|
|
441
|
+
</>
|
|
442
|
+
) : (
|
|
443
|
+
"Are you sure you want to clear all selections?"
|
|
444
|
+
)}
|
|
445
|
+
</DialogContentText>
|
|
446
|
+
</DialogContent>
|
|
447
|
+
<DialogActions sx={{ justifyContent: "center", gap: 2, pb: 2 }}>
|
|
448
|
+
<Button
|
|
449
|
+
variant="contained"
|
|
450
|
+
color="primary"
|
|
451
|
+
onClick={() => setClearDialogOpen(false)}
|
|
452
|
+
autoFocus
|
|
453
|
+
>
|
|
454
|
+
Cancel
|
|
455
|
+
</Button>
|
|
456
|
+
<Button
|
|
457
|
+
variant="outlined"
|
|
458
|
+
color="secondary"
|
|
459
|
+
onClick={confirmClear}
|
|
460
|
+
>
|
|
461
|
+
Clear
|
|
462
|
+
</Button>
|
|
463
|
+
</DialogActions>
|
|
464
|
+
</Dialog>
|
|
465
|
+
</Box>
|
|
466
|
+
)}
|
|
467
|
+
</DialogContent>
|
|
468
|
+
</Dialog>
|
|
371
469
|
);
|
|
372
470
|
}
|