@weng-lab/genomebrowser-ui 0.2.0-beta.2 → 0.2.0-beta.3
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/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +7 -3
- package/dist/TrackSelect/Folders/biosamples/shared/columns.d.ts +1 -1
- package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +4 -1
- package/dist/TrackSelect/Folders/types.d.ts +9 -1
- package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
- package/dist/TrackSelect/types.d.ts +12 -3
- package/dist/genomebrowser-ui.es.js +670 -597
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/package.json +1 -1
- package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +19 -6
- package/src/TrackSelect/Folders/biosamples/shared/columns.tsx +2 -2
- package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +9 -12
- package/src/TrackSelect/Folders/types.ts +12 -1
- package/src/TrackSelect/TrackSelect.tsx +28 -16
- package/src/TrackSelect/TreeView/CustomTreeItem.tsx +2 -5
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +44 -21
- package/src/TrackSelect/types.ts +14 -4
- package/test/main.tsx +2 -2
package/package.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FormControlLabel, Switch } from "@mui/material";
|
|
2
|
-
import { useState } from "react";
|
|
3
2
|
import { FolderRuntimeConfig } from "../../types";
|
|
4
3
|
import {
|
|
5
4
|
defaultColumns,
|
|
@@ -9,9 +8,13 @@ import {
|
|
|
9
8
|
sortedByAssayGroupingModel,
|
|
10
9
|
sortedByAssayLeafField,
|
|
11
10
|
} from "./columns";
|
|
11
|
+
import { buildTreeView, buildSortedAssayTreeView } from "./treeBuilder";
|
|
12
12
|
|
|
13
13
|
export interface AssayToggleProps {
|
|
14
14
|
updateConfig: (partial: Partial<FolderRuntimeConfig>) => void;
|
|
15
|
+
folderId: string;
|
|
16
|
+
label: string;
|
|
17
|
+
config: FolderRuntimeConfig;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -20,15 +23,21 @@ export interface AssayToggleProps {
|
|
|
20
23
|
*
|
|
21
24
|
* When toggled, it updates the folder's runtime config to switch:
|
|
22
25
|
* - columns: Different column definitions for each view
|
|
23
|
-
* - groupingModel: ["ontology", "displayName"] vs ["assay", "ontology"
|
|
24
|
-
* - leafField: "assay" vs "
|
|
26
|
+
* - groupingModel: ["ontology", "displayName"] vs ["assay", "ontology"]
|
|
27
|
+
* - leafField: "assay" vs "displayName"
|
|
28
|
+
* - buildTree: Different tree builder function
|
|
25
29
|
*/
|
|
26
|
-
export function AssayToggle({
|
|
27
|
-
|
|
30
|
+
export function AssayToggle({
|
|
31
|
+
updateConfig,
|
|
32
|
+
folderId,
|
|
33
|
+
label,
|
|
34
|
+
config,
|
|
35
|
+
}: AssayToggleProps) {
|
|
36
|
+
// Derive toggle state from current config's leafField
|
|
37
|
+
const sortedByAssay = config.leafField === sortedByAssayLeafField;
|
|
28
38
|
|
|
29
39
|
const handleToggle = () => {
|
|
30
40
|
const newValue = !sortedByAssay;
|
|
31
|
-
setSortedByAssay(newValue);
|
|
32
41
|
|
|
33
42
|
if (newValue) {
|
|
34
43
|
// Switch to assay-grouped view
|
|
@@ -36,6 +45,8 @@ export function AssayToggle({ updateConfig }: AssayToggleProps) {
|
|
|
36
45
|
columns: sortedByAssayColumns,
|
|
37
46
|
groupingModel: sortedByAssayGroupingModel,
|
|
38
47
|
leafField: sortedByAssayLeafField,
|
|
48
|
+
buildTree: (selectedIds, rowById) =>
|
|
49
|
+
buildSortedAssayTreeView(selectedIds, rowById, label, folderId),
|
|
39
50
|
});
|
|
40
51
|
} else {
|
|
41
52
|
// Switch back to default (sample-grouped) view
|
|
@@ -43,6 +54,8 @@ export function AssayToggle({ updateConfig }: AssayToggleProps) {
|
|
|
43
54
|
columns: defaultColumns,
|
|
44
55
|
groupingModel: defaultGroupingModel,
|
|
45
56
|
leafField: defaultLeafField,
|
|
57
|
+
buildTree: (selectedIds, rowById) =>
|
|
58
|
+
buildTreeView(selectedIds, rowById, label, folderId),
|
|
46
59
|
});
|
|
47
60
|
}
|
|
48
61
|
};
|
|
@@ -153,13 +153,13 @@ export const defaultColumns: GridColDef<BiosampleRowInfo>[] = [
|
|
|
153
153
|
];
|
|
154
154
|
|
|
155
155
|
/** Grouping model for sorted-by-assay view */
|
|
156
|
-
export const sortedByAssayGroupingModel = ["assay", "ontology"
|
|
156
|
+
export const sortedByAssayGroupingModel = ["assay", "ontology"];
|
|
157
157
|
|
|
158
158
|
/** Default grouping model (ontology-based) */
|
|
159
159
|
export const defaultGroupingModel = ["ontology", "displayName"];
|
|
160
160
|
|
|
161
161
|
/** Leaf field for sorted-by-assay view */
|
|
162
|
-
export const sortedByAssayLeafField = "
|
|
162
|
+
export const sortedByAssayLeafField = "displayName";
|
|
163
163
|
|
|
164
164
|
/** Default leaf field */
|
|
165
165
|
export const defaultLeafField = "assay";
|
|
@@ -35,7 +35,10 @@ function createRootNode(
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Builds tree in the sorted by assay view
|
|
38
|
-
* Hierarchy: Assay -> Ontology -> DisplayName
|
|
38
|
+
* Hierarchy: Assay -> Ontology -> DisplayName (leaf)
|
|
39
|
+
*
|
|
40
|
+
* This is the reverse of the default view - instead of grouping by sample first,
|
|
41
|
+
* we group by assay first, making displayName the leaf node.
|
|
39
42
|
*
|
|
40
43
|
* @param selectedIds - list of selected row IDs
|
|
41
44
|
* @param rowById - Mapping between an id and its BiosampleRowInfo object
|
|
@@ -55,6 +58,8 @@ export function buildSortedAssayTreeView(
|
|
|
55
58
|
string,
|
|
56
59
|
TreeViewBaseItem<ExtendedTreeItemProps>
|
|
57
60
|
>();
|
|
61
|
+
// Track which displayName nodes exist per assay+ontology combination
|
|
62
|
+
// and which experiment IDs they contain
|
|
58
63
|
const displayNameMap = new Map<
|
|
59
64
|
string,
|
|
60
65
|
TreeViewBaseItem<ExtendedTreeItemProps>
|
|
@@ -75,6 +80,7 @@ export function buildSortedAssayTreeView(
|
|
|
75
80
|
isAssayItem: true,
|
|
76
81
|
label: row.assay,
|
|
77
82
|
icon: "removeable",
|
|
83
|
+
assayName: row.assay, // Add assayName so the icon renders correctly
|
|
78
84
|
children: [],
|
|
79
85
|
allExpAccessions: [],
|
|
80
86
|
};
|
|
@@ -97,6 +103,7 @@ export function buildSortedAssayTreeView(
|
|
|
97
103
|
ontologyMap.set(ontologyKey, ontologyNode);
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
// DisplayName is now the leaf node (no children, no assay icon)
|
|
100
107
|
const displayNameKey = `${folderId}::${row.assay}-${row.ontology}-${row.displayName}`;
|
|
101
108
|
let displayNameNode = displayNameMap.get(displayNameKey);
|
|
102
109
|
if (!displayNameNode) {
|
|
@@ -112,17 +119,7 @@ export function buildSortedAssayTreeView(
|
|
|
112
119
|
displayNameMap.set(displayNameKey, displayNameNode);
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
id: row.id,
|
|
117
|
-
isAssayItem: false,
|
|
118
|
-
label: formatIdLabel(row.id),
|
|
119
|
-
icon: "removeable",
|
|
120
|
-
assayName: row.assay,
|
|
121
|
-
children: [],
|
|
122
|
-
allExpAccessions: [row.id],
|
|
123
|
-
};
|
|
124
|
-
displayNameNode.children!.push(expNode);
|
|
125
|
-
|
|
122
|
+
// Add this experiment ID to all parent nodes' allExpAccessions
|
|
126
123
|
assayNode.allExpAccessions!.push(row.id);
|
|
127
124
|
ontologyNode.allExpAccessions!.push(row.id);
|
|
128
125
|
displayNameNode.allExpAccessions!.push(row.id);
|
|
@@ -7,12 +7,17 @@ export type Assembly = "GRCh38" | "mm10";
|
|
|
7
7
|
/**
|
|
8
8
|
* Runtime configuration that can be modified by ToolbarExtras components.
|
|
9
9
|
* This allows folder-specific UI (like AssayToggle) to dynamically update
|
|
10
|
-
* how the DataGrid
|
|
10
|
+
* how the DataGrid and TreeView display data.
|
|
11
11
|
*/
|
|
12
12
|
export interface FolderRuntimeConfig {
|
|
13
13
|
columns: GridColDef[];
|
|
14
14
|
groupingModel: string[];
|
|
15
15
|
leafField: string;
|
|
16
|
+
/** Optional override for the tree builder function */
|
|
17
|
+
buildTree?: (
|
|
18
|
+
selectedIds: string[],
|
|
19
|
+
rowById: Map<string, any>,
|
|
20
|
+
) => TreeViewBaseItem<ExtendedTreeItemProps>[];
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/**
|
|
@@ -74,9 +79,15 @@ export interface FolderDefinition<TRow = any> {
|
|
|
74
79
|
* that switches between sample-grouped and assay-grouped views.
|
|
75
80
|
*
|
|
76
81
|
* @param updateConfig - Callback to update the folder's runtime config
|
|
82
|
+
* @param folderId - The folder's unique identifier
|
|
83
|
+
* @param label - The folder's display label
|
|
84
|
+
* @param config - The current runtime config for this folder
|
|
77
85
|
*/
|
|
78
86
|
ToolbarExtras?: React.FC<{
|
|
79
87
|
updateConfig: (partial: Partial<FolderRuntimeConfig>) => void;
|
|
88
|
+
folderId: string;
|
|
89
|
+
label: string;
|
|
90
|
+
config: FolderRuntimeConfig;
|
|
80
91
|
}>;
|
|
81
92
|
|
|
82
93
|
/**
|
|
@@ -167,23 +167,29 @@ export default function TrackSelect({
|
|
|
167
167
|
return Array.from(activeFolder.rowById.values());
|
|
168
168
|
}, [activeFolder]);
|
|
169
169
|
|
|
170
|
-
const
|
|
171
|
-
if (!activeFolder) return [];
|
|
170
|
+
const folderTrees = useMemo(() => {
|
|
172
171
|
return folders
|
|
173
172
|
.filter((folder) => {
|
|
174
173
|
const selected = selectedByFolder.get(folder.id);
|
|
175
174
|
return selected && selected.size > 0;
|
|
176
175
|
})
|
|
177
|
-
.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
.map((folder) => {
|
|
177
|
+
const config = runtimeConfigByFolder.get(folder.id);
|
|
178
|
+
const buildTree = config?.buildTree ?? folder.buildTree;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
folderId: folder.id,
|
|
182
|
+
items: attachFolderId(
|
|
183
|
+
buildTree(
|
|
184
|
+
Array.from(selectedByFolder.get(folder.id) ?? []),
|
|
185
|
+
folder.rowById,
|
|
186
|
+
),
|
|
187
|
+
folder.id,
|
|
182
188
|
),
|
|
183
|
-
folder.
|
|
184
|
-
|
|
185
|
-
);
|
|
186
|
-
}, [folders, selectedByFolder,
|
|
189
|
+
TreeItemComponent: folder.TreeItemComponent,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}, [folders, selectedByFolder, runtimeConfigByFolder]);
|
|
187
193
|
|
|
188
194
|
const updateActiveFolderConfig = useCallback(
|
|
189
195
|
(partial: Partial<FolderRuntimeConfig>) => {
|
|
@@ -335,9 +341,16 @@ export default function TrackSelect({
|
|
|
335
341
|
) : (
|
|
336
342
|
<Box />
|
|
337
343
|
)}
|
|
338
|
-
{currentView === "folder-detail" &&
|
|
339
|
-
|
|
340
|
-
|
|
344
|
+
{currentView === "folder-detail" &&
|
|
345
|
+
ToolbarExtras &&
|
|
346
|
+
activeConfig && (
|
|
347
|
+
<ToolbarExtras
|
|
348
|
+
updateConfig={updateActiveFolderConfig}
|
|
349
|
+
folderId={activeFolder.id}
|
|
350
|
+
label={activeFolder.label}
|
|
351
|
+
config={activeConfig}
|
|
352
|
+
/>
|
|
353
|
+
)}
|
|
341
354
|
</Box>
|
|
342
355
|
)}
|
|
343
356
|
|
|
@@ -365,10 +378,9 @@ export default function TrackSelect({
|
|
|
365
378
|
{/* Right panel - always visible */}
|
|
366
379
|
<Box sx={{ flex: 2, minWidth: 0 }}>
|
|
367
380
|
<TreeViewWrapper
|
|
368
|
-
|
|
381
|
+
folderTrees={folderTrees}
|
|
369
382
|
selectedCount={selectedCount}
|
|
370
383
|
onRemove={handleRemoveTreeItem}
|
|
371
|
-
TreeItemComponent={activeFolder.TreeItemComponent}
|
|
372
384
|
/>
|
|
373
385
|
</Box>
|
|
374
386
|
</Stack>
|
|
@@ -47,8 +47,8 @@ function CustomLabel({
|
|
|
47
47
|
renderIcon,
|
|
48
48
|
...other
|
|
49
49
|
}: CustomLabelProps) {
|
|
50
|
-
const variant =
|
|
51
|
-
const fontWeight =
|
|
50
|
+
const variant = "body2";
|
|
51
|
+
const fontWeight = 500;
|
|
52
52
|
const labelText = typeof children === "string" ? children : "";
|
|
53
53
|
return (
|
|
54
54
|
<TreeItemLabel
|
|
@@ -79,9 +79,6 @@ function CustomLabel({
|
|
|
79
79
|
alignItems="center"
|
|
80
80
|
sx={{ minWidth: 0, overflow: "hidden", flex: 1 }}
|
|
81
81
|
>
|
|
82
|
-
{isAssayItem && renderIcon && (
|
|
83
|
-
<Box sx={{ flexShrink: 0 }}>{renderIcon(other.id)}</Box>
|
|
84
|
-
)}
|
|
85
82
|
{assayName && renderIcon && (
|
|
86
83
|
<Box sx={{ flexShrink: 0 }}>{renderIcon(assayName)}</Box>
|
|
87
84
|
)}
|
|
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
4
4
|
import {
|
|
5
5
|
CustomTreeItemProps,
|
|
6
6
|
ExtendedTreeItemProps,
|
|
7
|
+
FolderTreeConfig,
|
|
7
8
|
TreeViewWrapperProps,
|
|
8
9
|
} from "../types";
|
|
9
10
|
import { CustomTreeItem } from "./CustomTreeItem";
|
|
@@ -24,12 +25,18 @@ function getAllExpandableItemIds(
|
|
|
24
25
|
return ids;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Internal component that renders a single folder's tree with its own expanded state.
|
|
30
|
+
*/
|
|
31
|
+
function FolderTree({
|
|
28
32
|
items,
|
|
29
|
-
selectedCount,
|
|
30
|
-
onRemove,
|
|
31
33
|
TreeItemComponent,
|
|
32
|
-
|
|
34
|
+
onRemove,
|
|
35
|
+
}: {
|
|
36
|
+
items: FolderTreeConfig["items"];
|
|
37
|
+
TreeItemComponent: FolderTreeConfig["TreeItemComponent"];
|
|
38
|
+
onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
|
|
39
|
+
}) {
|
|
33
40
|
const allExpandableIds = useMemo(
|
|
34
41
|
() => getAllExpandableItemIds(items),
|
|
35
42
|
[items],
|
|
@@ -56,6 +63,31 @@ export function TreeViewWrapper({
|
|
|
56
63
|
|
|
57
64
|
const TreeItem = TreeItemComponent ?? CustomTreeItem;
|
|
58
65
|
|
|
66
|
+
return (
|
|
67
|
+
<RichTreeView
|
|
68
|
+
items={items}
|
|
69
|
+
expandedItems={expandedItems}
|
|
70
|
+
onExpandedItemsChange={(_event, ids) => setExpandedItems(ids)}
|
|
71
|
+
slots={{ item: TreeItem }}
|
|
72
|
+
slotProps={{
|
|
73
|
+
item: {
|
|
74
|
+
onRemove: handleRemoveTreeItem,
|
|
75
|
+
} as Partial<CustomTreeItemProps>,
|
|
76
|
+
}}
|
|
77
|
+
sx={{
|
|
78
|
+
ml: 1,
|
|
79
|
+
mr: 1,
|
|
80
|
+
}}
|
|
81
|
+
itemChildrenIndentation={0}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function TreeViewWrapper({
|
|
87
|
+
folderTrees,
|
|
88
|
+
selectedCount,
|
|
89
|
+
onRemove,
|
|
90
|
+
}: TreeViewWrapperProps) {
|
|
59
91
|
return (
|
|
60
92
|
<Paper
|
|
61
93
|
sx={{
|
|
@@ -99,23 +131,14 @@ export function TreeViewWrapper({
|
|
|
99
131
|
overflow: "auto",
|
|
100
132
|
}}
|
|
101
133
|
>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} as Partial<CustomTreeItemProps>,
|
|
111
|
-
}}
|
|
112
|
-
sx={{
|
|
113
|
-
ml: 1,
|
|
114
|
-
mr: 1,
|
|
115
|
-
height: "100%",
|
|
116
|
-
}}
|
|
117
|
-
itemChildrenIndentation={0}
|
|
118
|
-
/>
|
|
134
|
+
{folderTrees.map((folderTree) => (
|
|
135
|
+
<FolderTree
|
|
136
|
+
key={folderTree.folderId}
|
|
137
|
+
items={folderTree.items}
|
|
138
|
+
TreeItemComponent={folderTree.TreeItemComponent}
|
|
139
|
+
onRemove={onRemove}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
119
142
|
</Box>
|
|
120
143
|
</Paper>
|
|
121
144
|
);
|
package/src/TrackSelect/types.ts
CHANGED
|
@@ -28,16 +28,26 @@ export type ExtendedTreeItemProps = {
|
|
|
28
28
|
allExpAccessions?: string[];
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Configuration for a single folder's tree in the TreeViewWrapper.
|
|
33
|
+
* Each folder gets its own tree with its own TreeItemComponent.
|
|
34
|
+
*/
|
|
35
|
+
export type FolderTreeConfig = {
|
|
36
|
+
folderId: string;
|
|
32
37
|
items: TreeViewBaseItem<ExtendedTreeItemProps>[];
|
|
33
|
-
|
|
34
|
-
onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
|
|
35
|
-
/** Optional custom TreeItem component */
|
|
38
|
+
/** Optional custom TreeItem component for this folder */
|
|
36
39
|
TreeItemComponent?: React.ForwardRefExoticComponent<
|
|
37
40
|
CustomTreeItemProps & React.RefAttributes<HTMLLIElement>
|
|
38
41
|
>;
|
|
39
42
|
};
|
|
40
43
|
|
|
44
|
+
export type TreeViewWrapperProps = {
|
|
45
|
+
/** Array of folder tree configurations, one per folder with selections */
|
|
46
|
+
folderTrees: FolderTreeConfig[];
|
|
47
|
+
selectedCount: number;
|
|
48
|
+
onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
41
51
|
export interface CustomLabelProps {
|
|
42
52
|
id: string;
|
|
43
53
|
children: React.ReactNode;
|
package/test/main.tsx
CHANGED
|
@@ -82,8 +82,8 @@ function Main() {
|
|
|
82
82
|
const currentAssembly: Assembly = "mm10";
|
|
83
83
|
|
|
84
84
|
const browserStore = createBrowserStoreMemo({
|
|
85
|
-
//
|
|
86
|
-
domain: { chromosome: "
|
|
85
|
+
// chr7:19,695,494-19,699,803
|
|
86
|
+
domain: { chromosome: "chr7", start: 19695494, end: 19699803 },
|
|
87
87
|
marginWidth: 100,
|
|
88
88
|
trackWidth: 1400,
|
|
89
89
|
multiplier: 3,
|