@weng-lab/genomebrowser-ui 0.3.6 → 0.4.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 -1
- package/dist/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +1 -13
- package/dist/TrackSelect/Folders/biosamples/shared/toTrack.d.ts +20 -0
- package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +4 -13
- package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +1 -3
- package/dist/TrackSelect/Folders/genes/shared/toTrack.d.ts +18 -0
- package/dist/TrackSelect/Folders/index.d.ts +6 -12
- package/dist/TrackSelect/Folders/mohd/data/human.json.d.ts +2948 -0
- package/dist/TrackSelect/Folders/mohd/human.d.ts +1 -0
- package/dist/TrackSelect/Folders/mohd/shared/MohdGroupingCell.d.ts +2 -0
- package/dist/TrackSelect/Folders/mohd/shared/MohdTreeItem.d.ts +3 -0
- package/dist/TrackSelect/Folders/mohd/shared/MohdViewSelector.d.ts +7 -0
- package/dist/TrackSelect/Folders/mohd/shared/columns.d.ts +5 -0
- package/dist/TrackSelect/Folders/mohd/shared/config.d.ts +42 -0
- package/dist/TrackSelect/Folders/mohd/shared/createFolder.d.ts +9 -0
- package/dist/TrackSelect/Folders/mohd/shared/toTrack.d.ts +9 -0
- package/dist/TrackSelect/Folders/mohd/shared/types.d.ts +40 -0
- package/dist/TrackSelect/Folders/other-tracks/shared/toTrack.d.ts +5 -0
- package/dist/TrackSelect/Folders/other-tracks/shared/types.d.ts +1 -0
- package/dist/TrackSelect/Folders/types.d.ts +23 -55
- package/dist/TrackSelect/TrackSelect.d.ts +10 -7
- package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
- package/dist/TrackSelect/buildSelectedTree.d.ts +15 -0
- package/dist/TrackSelect/managedTracks.d.ts +13 -0
- package/dist/TrackSelect/resolveFolderView.d.ts +2 -0
- package/dist/TrackSelect/trackContext.d.ts +5 -0
- package/dist/TrackSelect/types.d.ts +12 -33
- package/dist/genomebrowser-ui.es.js +2224 -1717
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/dist/lib.d.ts +4 -4
- package/dist/muiLicense.d.ts +1 -0
- package/package.json +6 -3
- package/src/TrackSelect/Dialogs/ResetDialog.tsx +3 -2
- package/src/TrackSelect/FolderList/FolderCard.tsx +1 -1
- package/src/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.tsx +33 -0
- package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +39 -58
- package/src/TrackSelect/Folders/biosamples/shared/toTrack.ts +138 -0
- package/src/TrackSelect/Folders/biosamples/shared/types.ts +4 -16
- package/src/TrackSelect/Folders/genes/shared/createFolder.ts +10 -31
- package/src/TrackSelect/Folders/genes/shared/toTrack.ts +59 -0
- package/src/TrackSelect/Folders/index.ts +14 -17
- package/src/TrackSelect/Folders/mohd/data/human.json +2945 -0
- package/src/TrackSelect/Folders/mohd/human.ts +10 -0
- package/src/TrackSelect/Folders/mohd/shared/MohdGroupingCell.tsx +68 -0
- package/src/TrackSelect/Folders/mohd/shared/MohdTreeItem.tsx +17 -0
- package/src/TrackSelect/Folders/mohd/shared/MohdViewSelector.tsx +33 -0
- package/src/TrackSelect/Folders/mohd/shared/columns.tsx +79 -0
- package/src/TrackSelect/Folders/mohd/shared/config.tsx +71 -0
- package/src/TrackSelect/Folders/mohd/shared/createFolder.ts +144 -0
- package/src/TrackSelect/Folders/mohd/shared/toTrack.ts +164 -0
- package/src/TrackSelect/Folders/mohd/shared/types.ts +46 -0
- package/src/TrackSelect/Folders/other-tracks/shared/createFolder.ts +13 -14
- package/src/TrackSelect/Folders/other-tracks/shared/toTrack.ts +17 -0
- package/src/TrackSelect/Folders/other-tracks/shared/types.ts +1 -0
- package/src/TrackSelect/Folders/types.ts +26 -69
- package/src/TrackSelect/TrackSelect.tsx +299 -255
- package/src/TrackSelect/TreeView/CustomTreeItem.tsx +6 -6
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +84 -6
- package/src/TrackSelect/buildSelectedTree.ts +145 -0
- package/src/TrackSelect/managedTracks.ts +92 -0
- package/src/TrackSelect/resolveFolderView.ts +20 -0
- package/src/TrackSelect/trackContext.ts +9 -0
- package/src/TrackSelect/types.ts +14 -39
- package/src/lib.ts +13 -7
- package/src/muiLicense.ts +9 -0
- package/src/vite-env.d.ts +9 -0
- package/test/TrackSelect.test.tsx +435 -0
- package/test/main.tsx +36 -352
- package/test/mocks/logo-test.tsx +11 -0
- package/test/mohdDisplay.test.tsx +45 -0
- package/test/startup.test.ts +206 -0
- package/test/trackSelectState.test.ts +176 -0
- package/vite.config.ts +1 -0
- package/vitest.config.ts +20 -0
- package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +0 -18
- package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +0 -28
- package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +0 -13
- package/dist/TrackSelect/Folders/other-tracks/shared/treeBuilder.d.ts +0 -4
- package/dist/TrackSelect/store.d.ts +0 -4
- package/src/TrackSelect/Folders/NEW.md +0 -929
- package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +0 -78
- package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +0 -224
- package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +0 -45
- package/src/TrackSelect/Folders/other-tracks/shared/treeBuilder.ts +0 -34
- 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 {
|
|
12
|
-
import {
|
|
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 {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
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
|
-
|
|
34
|
+
initialSelectedIds?: InitialSelectedIdsByAssembly;
|
|
35
|
+
sessionStorageKey?: string;
|
|
36
|
+
trackStore?: TrackStoreInstance;
|
|
27
37
|
onCancel?: () => void;
|
|
28
|
-
onClear?: () => void;
|
|
29
38
|
maxTracks?: number;
|
|
30
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
85
|
+
|
|
86
|
+
return normalized;
|
|
60
87
|
};
|
|
61
88
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
148
|
+
initialSelectedIds,
|
|
149
|
+
sessionStorageKey,
|
|
150
|
+
trackStore,
|
|
80
151
|
onCancel,
|
|
81
|
-
onClear,
|
|
82
152
|
maxTracks,
|
|
83
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const nextCommittedSelection = loadSelectedByFolder({
|
|
225
|
+
assembly,
|
|
226
|
+
folders,
|
|
227
|
+
initialSelectedIds,
|
|
228
|
+
sessionStorageKey,
|
|
161
229
|
});
|
|
162
|
-
|
|
163
|
-
|
|
230
|
+
setCommittedSelectedByFolder(nextCommittedSelection);
|
|
231
|
+
setSelectedByFolder(cloneSelectedByFolder(nextCommittedSelection));
|
|
232
|
+
}, [assembly, folders, initialSelectedIds, sessionStorageKey]);
|
|
164
233
|
|
|
165
|
-
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!open) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
166
238
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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)
|
|
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) =>
|
|
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
|
-
|
|
347
|
+
const nextSelectedByFolder = new Map(selectedByFolder);
|
|
348
|
+
nextSelectedByFolder.set(activeFolder.id, filteredIds);
|
|
349
|
+
setSelectedByFolder(nextSelectedByFolder);
|
|
242
350
|
};
|
|
243
351
|
|
|
244
352
|
const handleRemoveTreeItem = (
|
|
@@ -249,79 +357,21 @@ export default function TrackSelect({
|
|
|
249
357
|
return;
|
|
250
358
|
}
|
|
251
359
|
|
|
252
|
-
const
|
|
253
|
-
const nextSet = new Set(
|
|
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
|
-
|
|
365
|
+
nextSelectedByFolder.set(folderId, nextSet);
|
|
366
|
+
setSelectedByFolder(nextSelectedByFolder);
|
|
256
367
|
};
|
|
257
368
|
|
|
258
369
|
const handleSubmit = () => {
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
@@ -338,7 +388,7 @@ export default function TrackSelect({
|
|
|
338
388
|
<IconButton
|
|
339
389
|
size="large"
|
|
340
390
|
onClick={handleCancel}
|
|
341
|
-
sx={{ color: "white",
|
|
391
|
+
sx={{ color: "white", 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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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={
|
|
487
|
+
onClick={() => setClearDialogOpen(true)}
|
|
444
488
|
>
|
|
445
489
|
Clear
|
|
446
490
|
</Button>
|
|
447
|
-
{
|
|
491
|
+
{Boolean(trackStore) ? (
|
|
448
492
|
<Button
|
|
449
493
|
variant="outlined"
|
|
450
494
|
color="secondary"
|
|
451
495
|
size="small"
|
|
452
|
-
onClick={
|
|
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}>
|