@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
@@ -0,0 +1,435 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { Track, TrackType, createTrackStore } from "@weng-lab/genomebrowser";
4
+ import { act, type ReactElement } from "react";
5
+ import { createRoot, type Root } from "react-dom/client";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { buildSelectedTree } from "../src/TrackSelect/buildSelectedTree";
8
+ vi.mock("../src/TrackSelect/Folders/mohd/shared/MohdGroupingCell", () => ({
9
+ MohdGroupingCell: () => null,
10
+ }));
11
+ vi.mock("../src/TrackSelect/Folders/mohd/shared/MohdTreeItem", () => ({
12
+ MohdTreeItem: () => null,
13
+ }));
14
+ import TrackSelect from "../src/TrackSelect/TrackSelect";
15
+ import { FolderDefinition } from "../src/TrackSelect/Folders/types";
16
+ import { humanMohdFolder } from "../src/TrackSelect/Folders/mohd/human";
17
+ import { resolveFolderView } from "../src/TrackSelect/resolveFolderView";
18
+
19
+ (
20
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }
21
+ ).IS_REACT_ACT_ENVIRONMENT = true;
22
+
23
+ vi.mock("../src/TrackSelect/FolderList/Breadcrumb", () => ({
24
+ Breadcrumb: ({
25
+ currentFolder,
26
+ onNavigateToRoot,
27
+ }: {
28
+ currentFolder: { label: string } | null;
29
+ onNavigateToRoot: () => void;
30
+ }) => (
31
+ <button onClick={onNavigateToRoot}>
32
+ {currentFolder ? `breadcrumb:${currentFolder.label}` : "breadcrumb:root"}
33
+ </button>
34
+ ),
35
+ }));
36
+
37
+ vi.mock("../src/TrackSelect/FolderList/FolderList", () => ({
38
+ FolderList: ({
39
+ folders,
40
+ onFolderSelect,
41
+ }: {
42
+ folders: Array<{ id: string; label: string }>;
43
+ onFolderSelect: (folderId: string) => void;
44
+ }) => (
45
+ <div data-testid="folder-list">
46
+ {folders.map((folder) => (
47
+ <button key={folder.id} onClick={() => onFolderSelect(folder.id)}>
48
+ {folder.label}
49
+ </button>
50
+ ))}
51
+ </div>
52
+ ),
53
+ }));
54
+
55
+ vi.mock("../src/TrackSelect/DataGrid/DataGridWrapper", () => ({
56
+ DataGridWrapper: ({
57
+ leafField,
58
+ onSelectionChange,
59
+ rows,
60
+ selectedIds,
61
+ }: {
62
+ leafField: string;
63
+ onSelectionChange: (ids: Set<string>) => void;
64
+ rows: Array<{ id: string }>;
65
+ selectedIds: Set<string>;
66
+ }) => (
67
+ <div data-testid="data-grid">
68
+ <div data-testid="grid-leaf-field">{leafField}</div>
69
+ <div data-testid="grid-selected">{Array.from(selectedIds).join(",")}</div>
70
+ <button
71
+ onClick={() =>
72
+ onSelectionChange(new Set(rows.slice(0, 2).map((row) => row.id)))
73
+ }
74
+ >
75
+ select-first-two
76
+ </button>
77
+ </div>
78
+ ),
79
+ }));
80
+
81
+ vi.mock("../src/TrackSelect/TreeView/TreeViewWrapper", () => ({
82
+ TreeViewWrapper: ({
83
+ activeViewIdByFolder,
84
+ folders,
85
+ onRemove,
86
+ selectedByFolder,
87
+ selectedCount,
88
+ }: {
89
+ activeViewIdByFolder: Map<string, string>;
90
+ folders: Array<FolderDefinition<any>>;
91
+ onRemove: (item: { id: string; label: string }) => void;
92
+ selectedByFolder: Map<string, Set<string>>;
93
+ selectedCount: number;
94
+ }) => {
95
+ const flattenTreeLabels = (
96
+ items: Array<{ label: string; children?: Array<any> }>,
97
+ ): string[] => {
98
+ return items.flatMap((item) => [
99
+ item.label,
100
+ ...flattenTreeLabels(item.children ?? []),
101
+ ]);
102
+ };
103
+
104
+ const flattenLeafItems = (
105
+ items: Array<{
106
+ label: string;
107
+ children?: Array<any>;
108
+ allExpAccessions?: string[];
109
+ }>,
110
+ ): Array<{ id?: string; label: string }> => {
111
+ return items.flatMap((item) => {
112
+ const children = item.children ?? [];
113
+ const nestedLeaves = flattenLeafItems(children);
114
+ const isLeaf =
115
+ children.length === 0 && item.allExpAccessions?.length === 1;
116
+ return isLeaf ? [item, ...nestedLeaves] : nestedLeaves;
117
+ });
118
+ };
119
+
120
+ const folderTrees = folders.flatMap((folder) => {
121
+ const selectedIds = selectedByFolder.get(folder.id);
122
+ if (!selectedIds || selectedIds.size === 0) {
123
+ return [];
124
+ }
125
+
126
+ const activeView = resolveFolderView(folder, activeViewIdByFolder);
127
+ const selectedRows = folder.rows.filter((row) => selectedIds.has(row.id));
128
+
129
+ const attachFolderId = (items: Array<any>): Array<any> => {
130
+ return items.map((item) => ({
131
+ ...item,
132
+ folderId: folder.id,
133
+ children: item.children ? attachFolderId(item.children) : undefined,
134
+ }));
135
+ };
136
+
137
+ return [
138
+ attachFolderId(
139
+ buildSelectedTree({
140
+ folderId: folder.id,
141
+ rootLabel: folder.label,
142
+ selectedRows,
143
+ groupingModel: activeView.groupingModel,
144
+ leafField: activeView.leafField,
145
+ }),
146
+ ),
147
+ ];
148
+ });
149
+
150
+ const removableItem = folderTrees.flatMap((tree) =>
151
+ flattenLeafItems(tree),
152
+ )[1];
153
+
154
+ return (
155
+ <div data-testid="tree-view">
156
+ <div data-testid="selected-count">{selectedCount}</div>
157
+ <div data-testid="tree-labels">
158
+ {folderTrees.flatMap((tree) => flattenTreeLabels(tree)).join(",")}
159
+ </div>
160
+ {removableItem ? (
161
+ <button onClick={() => onRemove(removableItem)}>remove-second</button>
162
+ ) : null}
163
+ </div>
164
+ );
165
+ },
166
+ }));
167
+
168
+ vi.mock("../src/TrackSelect/Dialogs/ClearDialog", () => ({
169
+ ClearDialog: ({
170
+ open,
171
+ onConfirm,
172
+ }: {
173
+ open: boolean;
174
+ onConfirm: () => void;
175
+ }) => (open ? <button onClick={onConfirm}>confirm-clear</button> : null),
176
+ }));
177
+
178
+ vi.mock("../src/TrackSelect/Dialogs/ResetDialog", () => ({
179
+ ResetDialog: ({
180
+ open,
181
+ onConfirm,
182
+ }: {
183
+ open: boolean;
184
+ onConfirm: () => void;
185
+ }) => (open ? <button onClick={onConfirm}>confirm-reset</button> : null),
186
+ }));
187
+
188
+ vi.mock("../src/TrackSelect/Dialogs/LimitDialog", () => ({
189
+ LimitDialog: () => null,
190
+ }));
191
+
192
+ interface TestRow {
193
+ id: string;
194
+ label: string;
195
+ }
196
+
197
+ const createTestTrack = (id: string, title: string): Track =>
198
+ ({
199
+ id,
200
+ title,
201
+ height: 40,
202
+ trackType: TrackType.Custom,
203
+ }) as Track;
204
+
205
+ const createTestFolder = ({
206
+ id,
207
+ label,
208
+ rowIds,
209
+ withToolbarExtras = false,
210
+ }: {
211
+ id: string;
212
+ label: string;
213
+ rowIds: [string, string, string];
214
+ withToolbarExtras?: boolean;
215
+ }): FolderDefinition<TestRow> => {
216
+ const rows = [
217
+ { id: `${id}/${rowIds[0]}`, label: `${label} A` },
218
+ { id: `${id}/${rowIds[1]}`, label: `${label} B` },
219
+ { id: `${id}/${rowIds[2]}`, label: `${label} C` },
220
+ ];
221
+
222
+ const views = withToolbarExtras
223
+ ? [
224
+ {
225
+ id: "default",
226
+ label: "Default",
227
+ columns: [],
228
+ groupingModel: [],
229
+ leafField: "label",
230
+ },
231
+ {
232
+ id: "runtime",
233
+ label: "Runtime",
234
+ columns: [],
235
+ groupingModel: [],
236
+ leafField: "id",
237
+ },
238
+ ]
239
+ : undefined;
240
+
241
+ const ViewSelector = withToolbarExtras
242
+ ? ({
243
+ activeViewId,
244
+ onChange,
245
+ }: {
246
+ activeViewId: string;
247
+ onChange: (viewId: string) => void;
248
+ }) => (
249
+ <button
250
+ data-testid="toolbar-toggle"
251
+ onClick={() =>
252
+ onChange(activeViewId === "runtime" ? "default" : "runtime")
253
+ }
254
+ >
255
+ toolbar-toggle
256
+ </button>
257
+ )
258
+ : undefined;
259
+
260
+ return {
261
+ id,
262
+ label,
263
+ rows,
264
+ columns: [],
265
+ groupingModel: [],
266
+ leafField: "label",
267
+ createTrack: (row) => createTestTrack(row.id, row.label),
268
+ views,
269
+ ViewSelector,
270
+ };
271
+ };
272
+
273
+ let container: HTMLDivElement | null = null;
274
+ let root: Root | null = null;
275
+
276
+ afterEach(async () => {
277
+ if (root) {
278
+ await act(async () => {
279
+ root?.unmount();
280
+ });
281
+ }
282
+
283
+ root = null;
284
+ container?.remove();
285
+ container = null;
286
+ document.body.innerHTML = "";
287
+ });
288
+
289
+ const renderTrackSelect = async (ui: ReactElement) => {
290
+ container = document.createElement("div");
291
+ document.body.appendChild(container);
292
+ root = createRoot(container);
293
+
294
+ await act(async () => {
295
+ root?.render(ui);
296
+ });
297
+ };
298
+
299
+ const clickButton = async (label: string) => {
300
+ const button = Array.from(document.querySelectorAll("button")).find(
301
+ (candidate) => candidate.textContent?.trim() === label,
302
+ );
303
+
304
+ expect(button).toBeTruthy();
305
+
306
+ await act(async () => {
307
+ button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
308
+ });
309
+ };
310
+
311
+ const getText = (testId: string) => {
312
+ const element = document.querySelector(`[data-testid="${testId}"]`);
313
+ expect(element).toBeTruthy();
314
+ return element?.textContent ?? "";
315
+ };
316
+
317
+ describe("TrackSelect", () => {
318
+ it("keeps the modal flow wired through folder entry, local edits, dialogs, toolbar config, and submit", async () => {
319
+ const folderA = createTestFolder({
320
+ id: "folder-a",
321
+ label: "Folder A",
322
+ rowIds: ["managed-a", "managed-b", "managed-c"],
323
+ withToolbarExtras: true,
324
+ });
325
+ const folderB = createTestFolder({
326
+ id: "folder-b",
327
+ label: "Folder B",
328
+ rowIds: ["other-a", "other-b", "other-c"],
329
+ });
330
+ const trackStore = createTrackStore([
331
+ createTestTrack("external-track", "External Track"),
332
+ createTestTrack("folder-a/managed-a", "Managed A"),
333
+ ]);
334
+ const onClose = vi.fn();
335
+
336
+ trackStore.getState().editTrack("folder-a/managed-a", { height: 120 });
337
+ const committedManagedTrack = trackStore
338
+ .getState()
339
+ .getTrack("folder-a/managed-a");
340
+
341
+ await renderTrackSelect(
342
+ <TrackSelect
343
+ assembly="GRCh38"
344
+ folders={[folderA, folderB]}
345
+ initialSelectedIds={{
346
+ GRCh38: { [folderA.id]: ["folder-a/managed-a"] },
347
+ }}
348
+ open
349
+ onClose={onClose}
350
+ title="Track Select"
351
+ trackStore={trackStore}
352
+ />,
353
+ );
354
+
355
+ expect(getText("selected-count")).toBe("1");
356
+
357
+ await clickButton("Folder A");
358
+ expect(getText("grid-selected")).toBe("folder-a/managed-a");
359
+
360
+ await clickButton("toolbar-toggle");
361
+ expect(getText("grid-leaf-field")).toBe("id");
362
+
363
+ await clickButton("select-first-two");
364
+ expect(getText("grid-selected")).toBe(
365
+ "folder-a/managed-a,folder-a/managed-b",
366
+ );
367
+ expect(getText("selected-count")).toBe("2");
368
+
369
+ await clickButton("remove-second");
370
+ expect(getText("grid-selected")).toBe("folder-a/managed-a");
371
+ expect(getText("selected-count")).toBe("1");
372
+
373
+ await clickButton("Clear");
374
+ await clickButton("confirm-clear");
375
+ expect(getText("grid-selected")).toBe("");
376
+ expect(getText("selected-count")).toBe("0");
377
+
378
+ await clickButton("Reset");
379
+ await clickButton("confirm-reset");
380
+ expect(getText("grid-selected")).toBe("folder-a/managed-a");
381
+ expect(getText("selected-count")).toBe("1");
382
+
383
+ await clickButton("Submit");
384
+
385
+ expect(onClose).toHaveBeenCalledTimes(1);
386
+ expect(trackStore.getState().tracks.map((track) => track.id)).toEqual([
387
+ "external-track",
388
+ "folder-a/managed-a",
389
+ ]);
390
+ expect(trackStore.getState().getTrack("folder-a/managed-a")).toBe(
391
+ committedManagedTrack,
392
+ );
393
+ expect(trackStore.getState().getTrack("folder-a/managed-a")?.height).toBe(
394
+ 120,
395
+ );
396
+ });
397
+
398
+ it("switches MOHD between ome and site views without losing selection", async () => {
399
+ await renderTrackSelect(
400
+ <TrackSelect
401
+ assembly="GRCh38"
402
+ folders={[humanMohdFolder]}
403
+ initialSelectedIds={{
404
+ GRCh38: {
405
+ [humanMohdFolder.id]: [
406
+ "human-mohd/MOHD_EA100001::MOHD_EA100001_peaks-FDR5_GRCh38_v0.bigBed",
407
+ ],
408
+ },
409
+ }}
410
+ open
411
+ onClose={vi.fn()}
412
+ title="Track Select"
413
+ />,
414
+ );
415
+
416
+ expect(getText("grid-selected")).toBe(
417
+ "human-mohd/MOHD_EA100001::MOHD_EA100001_peaks-FDR5_GRCh38_v0.bigBed",
418
+ );
419
+ expect(getText("tree-labels")).toContain("MOHD,ATAC,CCH,MOHD_EA100001");
420
+
421
+ await clickButton("Site");
422
+
423
+ expect(getText("grid-selected")).toBe(
424
+ "human-mohd/MOHD_EA100001::MOHD_EA100001_peaks-FDR5_GRCh38_v0.bigBed",
425
+ );
426
+ expect(getText("tree-labels")).toContain("MOHD,CCH,ATAC,MOHD_EA100001");
427
+
428
+ await clickButton("Ome");
429
+
430
+ expect(getText("grid-selected")).toBe(
431
+ "human-mohd/MOHD_EA100001::MOHD_EA100001_peaks-FDR5_GRCh38_v0.bigBed",
432
+ );
433
+ expect(getText("tree-labels")).toContain("MOHD,ATAC,CCH,MOHD_EA100001");
434
+ });
435
+ });