Skip to content

Commit 8187b86

Browse files
Add itemGraph in ExportModal and depth control for related items
Add itemGraph in ExportModal and depth control for related items Add itemGraph in ExportModal and depth control for related items Use slider to control itemGraph depth Add cypress component tests Add cypress component tests Add cypress component tests Add cypress component tests Add cypress component tests Add cypress component tests
1 parent e3054d6 commit 8187b86

File tree

11 files changed

+373
-166
lines changed

11 files changed

+373
-166
lines changed

pydatalab/src/pydatalab/export_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ def create_eln_file(
112112
item_id: str | None = None,
113113
related_item_ids: list[str] | None = None,
114114
) -> None:
115+
if not collection_id and not item_id:
116+
raise ValueError("Either collection_id or item_id must be provided")
117+
115118
if collection_id:
116119
collection_data = flask_mongo.db.collections.find_one({"collection_id": collection_id})
117120
if not collection_data:

pydatalab/src/pydatalab/routes/v0_1/export.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,16 @@ def start_sample_export(item_id: str):
195195
related_item_ids = None
196196

197197
request_data = request.get_json() or {}
198-
if request_data.get("include_related") and request_data.get("related_item_ids"):
198+
if request_data.get("include_related"):
199+
related_item_ids = request_data.get("related_item_ids", [])
200+
if not related_item_ids:
201+
return jsonify(
202+
{
203+
"status": "error",
204+
"message": "related_item_ids required when include_related is true",
205+
}
206+
), 400
199207
export_type = "graph"
200-
related_item_ids = request_data["related_item_ids"]
201208

202209
export_task = ExportTask(
203210
task_id=task_id,

pydatalab/src/pydatalab/routes/v0_1/graphs.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ def get_graph_cy_format(
1717
item_id: str | None = None,
1818
collection_id: str | None = None,
1919
hide_collections: bool = True,
20+
max_depth: int = 1,
2021
):
2122
collection_id = request.args.get("collection_id", type=str)
2223
hide_collections = request.args.get(
2324
"hide_collections", default=True, type=lambda v: v.lower() == "true"
2425
)
26+
max_depth = request.args.get("max_depth", default=1, type=int)
2527

2628
if item_id is None:
2729
if collection_id is not None:
@@ -67,42 +69,59 @@ def get_graph_cy_format(
6769
404,
6870
)
6971

70-
all_documents = [main_item]
7172
node_ids = {item_id}
73+
all_documents = [main_item]
7274

73-
for relationship in main_item.get("relationships", []) or []:
74-
if relationship.get("item_id"):
75-
node_ids.add(relationship["item_id"])
75+
def add_related_items(current_item_id: str, current_depth: int):
76+
if current_depth > max_depth:
77+
return
7678

77-
incoming_items = list(
78-
flask_mongo.db.items.find(
79+
current_item = flask_mongo.db.items.find_one(
7980
{
80-
"$and": [
81-
{"relationships": {"$elemMatch": {"item_id": item_id}}},
82-
get_default_permissions(user_only=False),
83-
]
81+
"item_id": current_item_id,
82+
**get_default_permissions(user_only=False),
8483
},
8584
projection={"item_id": 1, "name": 1, "type": 1, "relationships": 1},
8685
)
87-
)
88-
89-
for doc in incoming_items:
90-
node_ids.add(doc["item_id"])
9186

92-
all_documents.extend(incoming_items)
93-
94-
ids_to_fetch = node_ids - {doc["item_id"] for doc in all_documents}
95-
if ids_to_fetch:
96-
referenced_items = list(
87+
if not current_item:
88+
return
89+
90+
for relationship in current_item.get("relationships", []) or []:
91+
if relationship.get("item_id") and relationship["item_id"] not in node_ids:
92+
node_ids.add(relationship["item_id"])
93+
related_item = flask_mongo.db.items.find_one(
94+
{
95+
"item_id": relationship["item_id"],
96+
**get_default_permissions(user_only=False),
97+
},
98+
projection={"item_id": 1, "name": 1, "type": 1, "relationships": 1},
99+
)
100+
if related_item:
101+
all_documents.append(related_item)
102+
add_related_items(relationship["item_id"], current_depth + 1)
103+
104+
incoming_items = list(
97105
flask_mongo.db.items.find(
98106
{
99-
"item_id": {"$in": list(ids_to_fetch)},
107+
"relationships": {
108+
"$elemMatch": {
109+
"item_id": current_item_id,
110+
}
111+
},
100112
**get_default_permissions(user_only=False),
101113
},
102114
projection={"item_id": 1, "name": 1, "type": 1, "relationships": 1},
103115
)
104116
)
105-
all_documents.extend(referenced_items)
117+
118+
for incoming_item in incoming_items:
119+
if incoming_item["item_id"] not in node_ids:
120+
node_ids.add(incoming_item["item_id"])
121+
all_documents.append(incoming_item)
122+
add_related_items(incoming_item["item_id"], current_depth + 1)
123+
124+
add_related_items(item_id, 1)
106125

107126
nodes = []
108127
edges = []

webapp/src/components/CollectionInformation.vue

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@
3030
v-model="CollectionDescription"
3131
aria-labelledby="description-label"
3232
></TinyMceInline>
33-
<div class="form-row">
34-
<div class="form-group col">
35-
<ExportButton :collection-id="collection_id" />
36-
</div>
37-
</div>
3833
</div>
3934
<div class="col-md-4">
4035
<CollectionRelationshipVisualization :collection_id="collection_id" />
@@ -66,15 +61,13 @@ import TinyMceInline from "@/components/TinyMceInline";
6661
import Creators from "@/components/Creators";
6762
import CollectionRelationshipVisualization from "@/components/CollectionRelationshipVisualization";
6863
import DynamicDataTable from "@/components/DynamicDataTable";
69-
import ExportButton from "@/components/ExportButton";
7064
7165
export default {
7266
components: {
7367
TinyMceInline,
7468
Creators,
7569
CollectionRelationshipVisualization,
7670
DynamicDataTable,
77-
ExportButton,
7871
},
7972
props: {
8073
collection_id: {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<template>
2+
<div class="nav-item dropdown">
3+
<a
4+
class="nav-link dropdown-toggle"
5+
role="button"
6+
data-toggle="dropdown"
7+
aria-haspopup="true"
8+
aria-expanded="false"
9+
@click="isDropdownVisible = !isDropdownVisible"
10+
>
11+
<i class="fa fa-download"></i> Export
12+
</a>
13+
<div v-show="isDropdownVisible" class="dropdown-menu" style="display: block">
14+
<a class="dropdown-item" @click="handleSimpleExport">
15+
<i class="fa fa-file"></i> Export {{ itemTypeLabel }} Only
16+
</a>
17+
<a v-if="itemType === 'samples'" class="dropdown-item" @click="handleGraphExport">
18+
<i class="fa fa-project-diagram"></i> Export Related Samples
19+
</a>
20+
</div>
21+
<ExportButton
22+
ref="exportButton"
23+
:item-id="itemId"
24+
:collection-id="collectionId"
25+
style="display: none"
26+
/>
27+
<SampleGraphExportModal
28+
v-if="itemType === 'samples'"
29+
ref="graphExportModal"
30+
:item-id="itemId"
31+
/>
32+
</div>
33+
</template>
34+
35+
<script>
36+
import ExportButton from "@/components/ExportButton";
37+
import SampleGraphExportModal from "@/components/SampleGraphExportModal";
38+
39+
export default {
40+
name: "ExportDropdown",
41+
components: {
42+
ExportButton,
43+
SampleGraphExportModal,
44+
},
45+
props: {
46+
itemId: {
47+
type: String,
48+
default: null,
49+
},
50+
collectionId: {
51+
type: String,
52+
default: null,
53+
},
54+
itemType: {
55+
type: String,
56+
required: true,
57+
},
58+
},
59+
data() {
60+
return {
61+
isDropdownVisible: false,
62+
};
63+
},
64+
computed: {
65+
itemTypeLabel() {
66+
const labels = {
67+
samples: "Sample",
68+
collections: "Collection",
69+
};
70+
return labels[this.itemType] || "Item";
71+
},
72+
},
73+
methods: {
74+
handleSimpleExport() {
75+
this.isDropdownVisible = false;
76+
this.$refs.exportButton.handleExport();
77+
},
78+
handleGraphExport() {
79+
this.isDropdownVisible = false;
80+
this.$refs.graphExportModal.show();
81+
},
82+
},
83+
};
84+
</script>

webapp/src/components/ItemGraph.vue

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
</div>
111111
</div>
112112
</div>
113-
<div id="cy" v-bind="$attrs" />
113+
<div ref="cyContainer" v-bind="$attrs" />
114114
</template>
115115

116116
<script>
@@ -195,6 +195,7 @@ export default {
195195
layoutIsRunning: true,
196196
};
197197
},
198+
198199
watch: {
199200
graphData() {
200201
this.generateCyNetworkPlot();
@@ -216,6 +217,13 @@ export default {
216217
.update();
217218
},
218219
},
220+
mounted() {
221+
if (this.graphData) {
222+
this.$nextTick(() => {
223+
this.generateCyNetworkPlot();
224+
});
225+
}
226+
},
219227
async created() {
220228
if (typeof this.cy !== "undefined") {
221229
this.generateCyNetworkPlot();
@@ -248,8 +256,13 @@ export default {
248256
if (!this.graphData) {
249257
return;
250258
}
259+
const cyElement = this.$refs.cyContainer;
260+
if (!cyElement) {
261+
console.warn("Cytoscape container not ready");
262+
return;
263+
}
251264
this.cy = cytoscape({
252-
container: document.getElementById("cy"),
265+
container: this.$refs.cyContainer,
253266
elements: this.graphData,
254267
userPanningEnabled: true,
255268
minZoom: 0.5,

0 commit comments

Comments
 (0)