diff --git a/pydatalab/pydatalab/routes/v0_1/items.py b/pydatalab/pydatalab/routes/v0_1/items.py
index e8e78d225..ad1890bdc 100644
--- a/pydatalab/pydatalab/routes/v0_1/items.py
+++ b/pydatalab/pydatalab/routes/v0_1/items.py
@@ -3,7 +3,7 @@
from typing import Dict, List, Optional, Set, Union
from bson import ObjectId
-from flask import Blueprint, jsonify, request
+from flask import Blueprint, jsonify, redirect, request
from flask_login import current_user
from pydantic import ValidationError
from pymongo.command_cursor import CommandCursor
@@ -644,6 +644,9 @@ def get_item_data(
call its render function).
"""
+ redirect_to_ui = bool(request.args.get("redirect-to-ui", default=False, type=json.loads))
+ if refcode and redirect_to_ui and CONFIG.APP_URL:
+ return redirect(f"{CONFIG.APP_URL}/items/{refcode}", code=307)
if item_id:
match = {"item_id": item_id}
@@ -767,6 +770,9 @@ def get_item_data(
# Must be exported to JSON first to apply the custom pydantic JSON encoders
return_dict = json.loads(doc.json(exclude_unset=True))
+ if item_id is None:
+ item_id = return_dict["item_id"]
+
# create the files_data dictionary keyed by file ObjectId
files_data: Dict[ObjectId, Dict] = {
f["immutable_id"]: f for f in return_dict.get("files") or []
diff --git a/pydatalab/tests/server/test_items.py b/pydatalab/tests/server/test_items.py
index c0bd1cba3..4ddf74ae7 100644
--- a/pydatalab/tests/server/test_items.py
+++ b/pydatalab/tests/server/test_items.py
@@ -2,13 +2,19 @@ def test_single_item_endpoints(client, inserted_default_items):
for item in inserted_default_items:
response = client.get(f"/items/{item.refcode}")
assert response.status_code == 200, response.json
+ assert response.json["item_id"] == item.item_id
+ assert response.json["item_data"]["item_id"] == item.item_id
assert response.json["status"] == "success"
test_ref = item.refcode.split(":")[1]
response = client.get(f"/items/{test_ref}")
assert response.status_code == 200, response.json
+ assert response.json["item_id"] == item.item_id
+ assert response.json["item_data"]["item_id"] == item.item_id
assert response.json["status"] == "success"
response = client.get(f"/get-item-data/{item.item_id}")
assert response.status_code == 200, response.json
assert response.json["status"] == "success"
+ assert response.json["item_id"] == item.item_id
+ assert response.json["item_data"]["item_id"] == item.item_id
diff --git a/webapp/package.json b/webapp/package.json
index c167d4397..872e880bc 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -1,6 +1,6 @@
{
"name": "datalab-vue",
- "version": "0.2.5",
+ "version": "0.4.5",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@@ -43,7 +43,9 @@
"vue-router": "^4.0.0-0",
"vue-select": "^4.0.0-beta.6",
"vue3-easy-data-table": "^1.5.45",
- "vuex": "^4.0.0-0"
+ "vuex": "^4.0.0-0",
+ "qrcode-vue3": "^1.6.8",
+ "vue-qrcode-reader": "^5.5.7"
},
"devDependencies": {
"@babel/core": "^7.24.8",
diff --git a/webapp/src/components/FormattedRefcode.vue b/webapp/src/components/FormattedRefcode.vue
index 679555225..d24eee322 100644
--- a/webapp/src/components/FormattedRefcode.vue
+++ b/webapp/src/components/FormattedRefcode.vue
@@ -1,4 +1,11 @@
+
+
+
{{ refcode }}
+
+
+
diff --git a/webapp/src/components/QRCodeModal.vue b/webapp/src/components/QRCodeModal.vue
new file mode 100644
index 000000000..ac17d222e
--- /dev/null
+++ b/webapp/src/components/QRCodeModal.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
diff --git a/webapp/src/components/QRScannerModal.vue b/webapp/src/components/QRScannerModal.vue
new file mode 100644
index 000000000..c087df506
--- /dev/null
+++ b/webapp/src/components/QRScannerModal.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/webapp/src/main.js b/webapp/src/main.js
index 5885aaef3..42493a4f7 100644
--- a/webapp/src/main.js
+++ b/webapp/src/main.js
@@ -15,6 +15,7 @@ import {
faCode,
faEnvelope,
faCog,
+ faQrcode,
faUsersCog,
faChevronRight,
faArrowUp,
@@ -51,6 +52,7 @@ library.add(
faSave,
faPen,
faCode,
+ faQrcode,
faEnvelope,
faCog,
faUsersCog,
diff --git a/webapp/src/resources.js b/webapp/src/resources.js
index e7169fe66..cb2d719eb 100644
--- a/webapp/src/resources.js
+++ b/webapp/src/resources.js
@@ -24,6 +24,10 @@ export const API_URL =
process.env.VUE_APP_API_URL != null ? process.env.VUE_APP_API_URL : "http://localhost:5001";
export const API_TOKEN = process.env.VUE_APP_API_TOKEN;
+export const QR_CODE_RESOLVER_URL = process.env.VUE_APP_QR_CODE_RESOLVER_URL;
+
+export const FEDERATION_QR_CODE_RESOLVER_URL = "https://purl.datalab-org.io";
+
export const LOGO_URL = process.env.VUE_APP_LOGO_URL;
export const HOMEPAGE_URL = process.env.VUE_APP_HOMEPAGE_URL;
diff --git a/webapp/src/router/index.js b/webapp/src/router/index.js
index e36ea6c15..502a8bff0 100644
--- a/webapp/src/router/index.js
+++ b/webapp/src/router/index.js
@@ -38,6 +38,11 @@ const routes = [
name: "edit",
component: EditPage,
},
+ {
+ path: "/items/:refcode",
+ name: "edit item",
+ component: EditPage,
+ },
{
path: "/starting-materials",
name: "starting-materials",
diff --git a/webapp/src/server_fetch_utils.js b/webapp/src/server_fetch_utils.js
index 8cf2f9591..ee98cd286 100644
--- a/webapp/src/server_fetch_utils.js
+++ b/webapp/src/server_fetch_utils.js
@@ -430,6 +430,23 @@ export async function getItemData(item_id) {
.catch((error) => alert("Error getting sample data: " + error));
}
+export async function getItemByRefcode(refcode) {
+ return fetch_get(`${API_URL}/items/${refcode}`)
+ .then((response_json) => {
+ store.commit("createItemData", {
+ refcode: refcode,
+ item_id: response_json.item_data.item_id,
+ item_data: response_json.item_data,
+ child_items: response_json.child_items,
+ parent_items: response_json.parent_items,
+ });
+ store.commit("updateFiles", response_json.files_data);
+
+ return "success";
+ })
+ .catch((error) => alert("Error getting item data: " + error));
+}
+
export async function getCollectionData(collection_id) {
return fetch_get(`${API_URL}/collections/${collection_id}`)
.then((response_json) => {
diff --git a/webapp/src/store/index.js b/webapp/src/store/index.js
index fed93173b..b565c613c 100644
--- a/webapp/src/store/index.js
+++ b/webapp/src/store/index.js
@@ -11,6 +11,7 @@ export default createStore({
all_collection_data: {},
all_collection_children: {},
all_collection_parents: {},
+ refcode_to_id: {},
sample_list: [],
equipment_list: [],
starting_material_list: [],
@@ -113,12 +114,13 @@ export default createStore({
},
createItemData(state, payload) {
// payload should have the following fields:
- // item_id, item_data, child_items, parent_items
+ // refcode, item_id, item_data, child_items, parent_items
// Object.assign(state.all_sample_data[payload.item_data], payload.item_data)
state.all_item_data[payload.item_id] = payload.item_data;
state.all_item_children[payload.item_id] = payload.child_items;
state.all_item_parents[payload.item_id] = payload.parent_items;
state.saved_status_items[payload.item_id] = true;
+ state.refcode_to_id[payload.refcode] = payload.item_id;
},
setCollectionData(state, payload) {
// payload should have the following fields:
diff --git a/webapp/src/views/EditPage.vue b/webapp/src/views/EditPage.vue
index 4a5e007dc..54c1af292 100644
--- a/webapp/src/views/EditPage.vue
+++ b/webapp/src/views/EditPage.vue
@@ -96,6 +96,7 @@ import FileList from "@/components/FileList";
import FileSelectModal from "@/components/FileSelectModal";
import {
getItemData,
+ getItemByRefcode,
addABlock,
saveItem,
updateBlockFromServer,
@@ -137,7 +138,8 @@ export default {
},
data() {
return {
- item_id: this.$route.params.id,
+ item_id: this.$route.params?.id || null,
+ refcode: this.$route.params?.refcode || null,
itemDataLoaded: false,
isMenuDropdownVisible: false,
selectedRemoteFiles: [],
@@ -149,7 +151,7 @@ export default {
},
computed: {
itemType() {
- return this.$store.state.all_item_data[this.item_id]?.type;
+ return this.$store.state.all_item_data[this.item_id]?.type || null;
},
itemTypeEntry() {
return itemTypes[this.itemType] || null;
@@ -158,7 +160,7 @@ export default {
return this.itemTypeEntry?.navbarColor || "DarkGrey";
},
item_data() {
- return this.$store.state.all_item_data[this.item_id] || {};
+ return this.$store.state.all_item_data[this.item_id] || { display_order: [] };
},
blocks() {
return this.item_data.blocks_obj;
@@ -185,6 +187,9 @@ export default {
blocksInfos() {
return this.$store.state.blocksInfos;
},
+ itemApiUrl() {
+ return API_URL + "/items/" + this.refcode;
+ },
},
watch: {
// add a warning before leaving page if unsaved
@@ -203,7 +208,6 @@ export default {
},
beforeMount() {
this.blockTypes = blockTypes; // bind blockTypes as a NON-REACTIVE object to the this context so that it is accessible by the template.
- this.itemApiUrl = API_URL + "/get-item-data/" + this.item_id;
},
mounted() {
// overwrite ctrl-s and cmd-s to save the page
@@ -277,16 +281,23 @@ export default {
this.lastModified = "just now";
},
getSampleData() {
- getItemData(this.item_id).then(() => {
- this.itemDataLoaded = true;
-
- // update each block asynchronously
- this.item_data.display_order.forEach((block_id) => {
- console.log(`calling update on block ${block_id}`);
- updateBlockFromServer(this.item_id, block_id, this.item_data.blocks_obj[block_id]);
+ if (this.item_id == null) {
+ getItemByRefcode(this.refcode).then(() => {
+ this.item_id = this.$store.state.refcode_to_id[this.refcode];
+ });
+ } else {
+ getItemData(this.item_id).then(() => {
+ this.refcode = this.item_data.refcode;
});
- this.setLastModified();
+ }
+ this.itemDataLoaded = true;
+
+ // update each block asynchronously
+ this.item_data.display_order.forEach((block_id) => {
+ console.log(`calling update on block ${block_id}`);
+ updateBlockFromServer(this.item_id, block_id, this.item_data.blocks_obj[block_id]);
});
+ this.setLastModified();
},
leavePageWarningListener(event) {
event.preventDefault;
diff --git a/webapp/src/views/Samples.vue b/webapp/src/views/Samples.vue
index fadbc428a..c67d00272 100644
--- a/webapp/src/views/Samples.vue
+++ b/webapp/src/views/Samples.vue
@@ -8,6 +8,9 @@
+
@@ -18,6 +21,7 @@
+