Repository: datalab-org/datalab
Pull request #1163: Quality of life updates to the inventory: barcode display, richer data table
Author: login: ml-evs
Labels: name: webapp
description: For issues/PRs pertaining to the web interface
name: polish
description: features that aren't useful but add polish
name: blocked
Created: 2025-05-01T13:53:11Z | Merged: 2025-05-07T22:09:10Z

This PR improves the inventory table with:

- [x] barcode display
- [x] additional data table filtering and display

Also:
- closes #1174 by properly restricting types in item search.
- ~closes #1175 by allowing refcodes to be passed to create sample, but only to check if they already exist~

Linked issue #1174: `/search-items` endpoint is returning non-matching types
e.g., I am seeing equipment returned when the query is requesting only samples.

Diff:
diff --git a/pydatalab/src/pydatalab/routes/v0_1/items.py b/pydatalab/src/pydatalab/routes/v0_1/items.py
index 6f69391f4..a0528e32e 100644
--- a/pydatalab/src/pydatalab/routes/v0_1/items.py
+++ b/pydatalab/src/pydatalab/routes/v0_1/items.py
@@ -126,6 +126,7 @@ def get_starting_materials():
                     "$project": {
                         "_id": 0,
                         "item_id": 1,
+                        "blocks": {"blocktype": 1, "title": 1},
                         "nblocks": {"$size": "$display_order"},
                         "nfiles": {"$size": "$file_ObjectIds"},
                         "date": 1,
@@ -133,6 +134,8 @@ def get_starting_materials():
                         "name": 1,
                         "type": 1,
                         "chemical_purity": 1,
+                        "barcode": 1,
+                        "refcode": 1,
                         "supplier": 1,
                         "location": 1,
                     }
@@ -312,18 +315,22 @@ def search_items():
     if isinstance(query, str) and query.startswith("%"):
         query = query.lstrip("%")
         match_obj = {
-            "$match": {
-                "$text": {"$search": query},
-                **get_default_permissions(user_only=False),
-            }
+            "$text": {"$search": query},
+            **get_default_permissions(user_only=False),
         }
-        pipeline.append(match_obj)
+        if types is not None:
+            match_obj["type"] = {"$in": types}
+
+        pipeline.append({"$match": match_obj})
         pipeline.append({"$sort": {"score": {"$meta": "textScore"}}})
     else:
         match_obj = {
             "$or": [{field: {"$regex": query, "$options": "i"}} for field in ITEMS_FTS_FIELDS]
         }
         match_obj = {"$and": [get_default_permissions(user_only=False), match_obj]}
+        if types is not None:
+            match_obj["$and"].append({"type": {"$in": types}})
+
         pipeline.append({"$match": match_obj})
 
     pipeline.append({"$limit": nresults})
diff --git a/pydatalab/tests/server/test_samples.py b/pydatalab/tests/server/test_samples.py
index 97f70d237..cd7017f20 100644
--- a/pydatalab/tests/server/test_samples.py
+++ b/pydatalab/tests/server/test_samples.py
@@ -193,7 +193,7 @@ def test_item_search(client, admin_client, real_mongo_client, example_items):
     assert response.json["items"][0]["item_id"] == "material"
 
     # Test regex search
-    response = client.get("/search-items/?query=mater")
+    response = client.get("/search-items/?query=mater&types=samples,starting_materials")
 
     assert response.status_code == 200
     assert response.json["status"] == "success"
@@ -202,6 +202,14 @@ def test_item_search(client, admin_client, real_mongo_client, example_items):
     assert "12345" in item_ids
     assert len(item_ids) == 2
 
+    # Test regex search with different types
+    response = client.get("/search-items/?query=mater&types=equipment")
+
+    assert response.status_code == 200
+    assert response.json["status"] == "success"
+    item_ids = {item["item_id"] for item in response.json["items"]}
+    assert len(item_ids) == 0
+
     # Test regex search still obeys permissions
     response = admin_client.get("/search-items/?query=mater")
 
diff --git a/webapp/cypress/component/StartingMaterialTableTest.cy.jsx b/webapp/cypress/component/StartingMaterialTableTest.cy.jsx
index 28f2cb223..4bd9a2c82 100644
--- a/webapp/cypress/component/StartingMaterialTableTest.cy.jsx
+++ b/webapp/cypress/component/StartingMaterialTableTest.cy.jsx
@@ -29,6 +29,8 @@ describe("StartingMaterialTable Component Tests", () => {
               chemform: "H2O",
               date: "2023-09-01",
               chemical_purity: "99%",
+              location: "King's Lynn",
+              barcode: "123",
               nblocks: 1,
             },
             {
@@ -37,6 +39,8 @@ describe("StartingMaterialTable Component Tests", () => {
               name: "Material Two",
               chemform: "CH4",
               date: "2023-08-15",
+              barcode: "456",
+              location: "Hunstanton",
               chemical_purity: "95%",
               nblocks: 2,
             },
@@ -75,10 +79,11 @@ describe("StartingMaterialTable Component Tests", () => {
     const headers = [
       "", // checkbox
       "ID",
+      "", // barcode
       "Name",
       "Formula",
       "Date",
-      "Purity",
+      "Location",
       "", // nblocks
       "", //nfiles
     ];
@@ -96,11 +101,12 @@ describe("StartingMaterialTable Component Tests", () => {
       .within(() => {
         cy.get("td").eq(0).should("contain.text", "");
         cy.get("td").eq(1).should("contain.text", "material1");
-        cy.get("td").eq(2).should("contain.text", "Material One");
-        cy.get("td").eq(3).should("contain.text", "H2O");
-        cy.get("td").eq(4).should("contain.text", "9/1/2023");
-        cy.get("td").eq(5).should("contain.text", "99%");
-        cy.get("td").eq(6).should("contain.text", "1");
+        cy.get("td").eq(2).should("contain.text", "123");
+        cy.get("td").eq(3).should("contain.text", "Material One");
+        cy.get("td").eq(4).should("contain.text", "H2O");
+        cy.get("td").eq(5).should("contain.text", "9/1/2023");
+        cy.get("td").eq(6).should("contain.text", "King's Lynn");
+        cy.get("td").eq(7).should("contain.text", "1");
       });
 
     cy.get(".p-datatable-tbody")
@@ -109,11 +115,12 @@ describe("StartingMaterialTable Component Tests", () => {
       .within(() => {
         cy.get("td").eq(0).should("contain.text", "");
         cy.get("td").eq(1).should("contain.text", "material2");
-        cy.get("td").eq(2).should("contain.text", "Material Two");
-        cy.get("td").eq(3).should("contain.text", "CH4");
-        cy.get("td").eq(4).should("contain.text", "8/15/2023");
-        cy.get("td").eq(5).should("contain.text", "95%");
-        cy.get("td").eq(6).should("contain.text", "2");
+        cy.get("td").eq(2).should("contain.text", "456");
+        cy.get("td").eq(3).should("contain.text", "Material Two");
+        cy.get("td").eq(4).should("contain.text", "CH4");
+        cy.get("td").eq(5).should("contain.text", "8/15/2023");
+        cy.get("td").eq(6).should("contain.text", "Hunstanton");
+        cy.get("td").eq(7).should("contain.text", "2");
       });
   });
 
@@ -129,4 +136,17 @@ describe("StartingMaterialTable Component Tests", () => {
         cy.get("td").eq(1).find(".formatted-item-name").should("exist");
       });
   });
+
+  it("renders the component FormattedBarcode", () => {
+    cy.get(".p-datatable-tbody tr")
+      .eq(0)
+      .within(() => {
+        cy.get("td").eq(2).find("[data-testid='formatted-barcode']").should("exist");
+      });
+    cy.get(".p-datatable-tbody tr")
+      .eq(1)
+      .within(() => {
+        cy.get("td").eq(2).find("[data-testid='formatted-barcode']").should("exist");
+      });
+  });
 });
diff --git a/webapp/src/App.vue b/webapp/src/App.vue
index 7a3b20643..22de33feb 100644
--- a/webapp/src/App.vue
+++ b/webapp/src/App.vue
@@ -12,6 +12,7 @@ body {
 }
 
 @import url("https://fonts.googleapis.com/css?family=Figtree");
+@import url("https://fonts.googleapis.com/css?family=Libre+Barcode+39+Text");
 @import url("https://fonts.googleapis.com/css?family=Roboto+Mono");
 #app {
   font-family: Figtree, Avenir, Arial, sans-serif;
diff --git a/webapp/src/components/BarcodeModal.vue b/webapp/src/components/BarcodeModal.vue
new file mode 100644
index 000000000..5c83ab8a2
--- /dev/null
+++ b/webapp/src/components/BarcodeModal.vue
@@ -0,0 +1,40 @@
+<template>
+  <form class="modal-enclosure" data-testid="barcode-form">
+    <Modal
+      :model-value="modelValue"
+      :disable-submit="true"
+      @update:model-value="$emit('update:modelValue', $event)"
+    >
+      <template #header>Barcode</template>
+      <template #body>
+        <div class="form-row">
+          <div ref="barcode" class="form-group mx-auto barcode-display" data-testid="barcode">
+            {{ barcode }}
+          </div>
+        </div>
+      </template>
+    </Modal>
+  </form>
+</template>
+
+<script>
+import Modal from "@/components/Modal.vue";
+export default {
+  name: "BarcodeModal",
+  components: {
+    Modal,
+  },
+  props: {
+    modelValue: Boolean,
+    barcode: { type: String, required: true },
+  },
+  emits: ["update:modelValue"],
+};
+</script>
+
+<style scoped>
+.barcode-display {
+  font-family: "Libre Barcode 39 Text";
+  font-size: 5rem;
+}
+</style>
diff --git a/webapp/src/components/CollectionInformation.vue b/webapp/src/components/CollectionInformation.vue
index da878bb91..9211a6197 100644
--- a/webapp/src/components/CollectionInformation.vue
+++ b/webapp/src/components/CollectionInformation.vue
@@ -35,7 +35,14 @@
       :data="children"
       :columns="collectionTableColumns"
       :data-type="'samples'"
-      :global-filter-fields="['item_id', 'type', 'name', 'chemform', 'creatorsList', 'nblocks']"
+      :global-filter-fields="[
+        'item_id',
+        'name',
+        'refcode',
+        'chemform',
+        'blocks',
+        'characteristic_chemical_formula',
+      ]"
       :show-buttons="false"
     />
   </div>
diff --git a/webapp/src/components/CollectionTable.vue b/webapp/src/components/CollectionTable.vue
index f006fc202..8a51c6380 100644
--- a/webapp/src/components/CollectionTable.vue
+++ b/webapp/src/components/CollectionTable.vue
@@ -3,7 +3,7 @@
     :columns="collectionColumn"
     :data="collections"
     :data-type="'collections'"
-    :global-filter-fields="['collection_id', 'title', 'creatorsList']"
+    :global-filter-fields="['collection_id', 'title']"
     :edit-page-route-prefix="'collections'"
   />
 </template>
diff --git a/webapp/src/components/CompactConstituentTable.vue b/webapp/src/components/CompactConstituentTable.vue
index d5ab4e738..14ddcf1b3 100644
--- a/webapp/src/components/CompactConstituentTable.vue
+++ b/webapp/src/components/CompactConstituentTable.vue
@@ -136,6 +136,9 @@ export default {
     },
     constituents: {
       get() {
+        if (!this.modelValue) {
+          return [];
+        }
         return this.modelValue;
       },
       set(value) {
diff --git a/webapp/src/components/DynamicDataTable.vue b/webapp/src/components/DynamicDataTable.vue
index c5127776b..49481d7d8 100644
--- a/webapp/src/components/DynamicDataTable.vue
+++ b/webapp/src/components/DynamicDataTable.vue
@@ -247,6 +247,8 @@ import ChemicalFormula from "@/components/ChemicalFormula";
 import CollectionList from "@/components/CollectionList";
 import Creators from "@/components/Creators";
 import UserBubble from "@/components/UserBubble.vue";
+import FormattedBarcode from "@/components/FormattedBarcode.vue";
+import FormattedRefcode from "@/components/FormattedRefcode.vue";
 
 import { FilterMatchMode, FilterOperator, FilterService } from "@primevue/core/api";
 import DataTable from "primevue/datatable";
@@ -262,6 +264,8 @@ export default {
     FilesIconCounter,
     BatchCreateItemModal,
     QRScannerModal,
+    FormattedBarcode,
+    FormattedRefcode,
     CreateCollectionModal,
     CreateEquipmentModal,
     AddToCollectionModal,
@@ -329,6 +333,10 @@ export default {
           operator: FilterOperator.AND,
           constraints: [{ value: null, matchMode: "exactTypeMatch" }],
         },
+        location: {
+          operator: FilterOperator.AND,
+          constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }],
+        },
         collections: {
           operator: FilterOperator.AND,
           constraints: [{ value: null, matchMode: "exactCollectionMatch" }],
@@ -563,6 +571,15 @@ export default {
           itemType: "type",
           enableModifiedClick: true,
         },
+        FormattedRefcode: {
+          enableQRCode: false,
+          refcode: "refcode",
+        },
+        FormattedBarcode: {
+          enableBarcode: false,
+          enableModifiedClick: false,
+          barcode: "barcode",
+        },
         FormattedCollectionName: {
           collection_id: "collection_id",
           enableModifiedClick: true,
diff --git a/webapp/src/components/EquipmentTable.vue b/webapp/src/components/EquipmentTable.vue
index 30d10ea05..e93ab64f0 100644
--- a/webapp/src/components/EquipmentTable.vue
+++ b/webapp/src/components/EquipmentTable.vue
@@ -3,7 +3,7 @@
     :columns="equipmentColumn"
     :data="equipment"
     :data-type="'equipment'"
-    :global-filter-fields="['item_id', 'name', 'location', 'creatorsList']"
+    :global-filter-fields="['item_id', 'name', 'location', 'refcode']"
   />
 </template>
 
diff --git a/webapp/src/components/FormattedBarcode.vue b/webapp/src/components/FormattedBarcode.vue
new file mode 100644
index 000000000..46654e0de
--- /dev/null
+++ b/webapp/src/components/FormattedBarcode.vue
@@ -0,0 +1,61 @@
+<template>
+  <span v-if="barcode">
+    <font-awesome-icon
+      v-if="enableBarcode"
+      icon="barcode"
+      class="badge clickable"
+      title="Click to show Barcode for this item"
+      aria-label="Click to show Barcode for this item"
+      @click="BarcodeModalOpen = true"
+    />
+    <font-awesome-icon v-else icon="barcode" class="badge" />
+    <span
+      class="badge clickable"
+      data-testid="formatted-barcode"
+      :style="{ backgroundColor: badgeColor }"
+    >
+      {{ barcode }}
+    </span>
+    <div v-if="enableBarcode">
+      <BarcodeModal v-model="BarcodeModalOpen" :barcode="barcode" />
+    </div>
+  </span>
+</template>
+
+<script>
+import BarcodeModal from "@/components/BarcodeModal.vue";
+
+export default {
+  components: {
+    BarcodeModal,
+  },
+  props: {
+    barcode: {
+      type: String,
+      required: true,
+    },
+    enableBarcode: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  data() {
+    return {
+      BarcodeModalOpen: false,
+    };
+  },
+  computed: {
+    badgeColor() {
+      return "LightGrey";
+    },
+  },
+};
+</script>
+
+<style scoped>
+.badge {
+  color: black;
+  text-align: center;
+  vertical-align: middle;
+}
+</style>
diff --git a/webapp/src/components/FormattedRefcode.vue b/webapp/src/components/FormattedRefcode.vue
index d24eee322..3409bbc09 100644
--- a/webapp/src/components/FormattedRefcode.vue
+++ b/webapp/src/components/FormattedRefcode.vue
@@ -16,7 +16,9 @@
   >
     {{ refcode }}
   </span>
-  <QRCodeModal v-model="QRCodeModalOpen" :refcode="refcode" />
+  <div v-if="enableQRCode">
+    <QRCodeModal v-model="QRCodeModalOpen" :refcode="refcode" />
+  </div>
 </template>
 
 <script>
diff --git a/webapp/src/components/SampleTable.vue b/webapp/src/components/SampleTable.vue
index 74c4f2eb8..aa4865cf1 100644
--- a/webapp/src/components/SampleTable.vue
+++ b/webapp/src/components/SampleTable.vue
@@ -5,12 +5,11 @@
     :data-type="'samples'"
     :global-filter-fields="[
       'item_id',
-      'type',
       'name',
+      'refcode',
       'chemform',
-      'collectionsList',
-      'creatorsList',
-      'nblocks',
+      'blocks',
+      'characteristic_chemical_formula',
     ]"
   />
 </template>
diff --git a/webapp/src/components/StartingMaterialInformation.vue b/webapp/src/components/StartingMaterialInformation.vue
index 62f5adf2b..e8a3ca59f 100644
--- a/webapp/src/components/StartingMaterialInformation.vue
+++ b/webapp/src/components/StartingMaterialInformation.vue
@@ -30,6 +30,10 @@
             <label for="startmat-refcode">Refcode</label>
             <div id="startmat-refcode"><FormattedRefcode :refcode="Refcode" /></div>
           </div>
+          <div v-if="Barcode" class="form-group col-md-3 col-sm-4 col-6">
+            <label for="startmat-barcode">Barcode</label>
+            <div id="startmat-barcode"><FormattedBarCode :barcode="Barcode" /></div>
+          </div>
           <div class="form-group col-md-6 col-sm-7 pr-2">
             <ToggleableCollectionFormGroup v-model="Collections" />
           </div>
@@ -100,6 +104,7 @@ import ChemFormulaInput from "@/components/ChemFormulaInput";
 import TableOfContents from "@/components/TableOfContents";
 import ToggleableCollectionFormGroup from "@/components/ToggleableCollectionFormGroup";
 import FormattedRefcode from "@/components/FormattedRefcode";
+import FormattedBarCode from "@/components/FormattedBarcode";
 import StyledInput from "@/components/StyledInput";
 import SynthesisInformation from "@/components/SynthesisInformation";
 import ItemRelationshipVisualization from "@/components/ItemRelationshipVisualization";
@@ -117,6 +122,7 @@ export default {
     ToggleableCollectionFormGroup,
     TableOfContents,
     FormattedRefcode,
+    FormattedBarCode,
     SynthesisInformation,
     GHSHazardInformation,
   },
@@ -149,6 +155,7 @@ export default {
     ItemDescription: createComputedSetterForItemField("description"),
     Collections: createComputedSetterForItemField("collections"),
     Refcode: createComputedSetterForItemField("refcode"),
+    Barcode: createComputedSetterForItemField("barcode"),
   },
   created() {
     this.isEditable = EDITABLE_INVENTORY;
diff --git a/webapp/src/components/StartingMaterialTable.vue b/webapp/src/components/StartingMaterialTable.vue
index a46159b58..68274bfd0 100644
--- a/webapp/src/components/StartingMaterialTable.vue
+++ b/webapp/src/components/StartingMaterialTable.vue
@@ -3,7 +3,16 @@
     :columns="startingMaterialColumn"
     :data="startingMaterials"
     :data-type="'startingMaterials'"
-    :global-filter-fields="['item_id', 'name', 'chemform', 'chemical_purity', 'nblocks']"
+    :global-filter-fields="[
+      'item_id',
+      'barcode',
+      'refcode',
+      'name',
+      'chemform',
+      'blocks',
+      'supplier',
+      'location',
+    ]"
   />
 </template>
 
@@ -17,16 +26,24 @@ export default {
     return {
       startingMaterialColumn: [
         { field: "item_id", header: "ID", body: "FormattedItemName", filter: true, label: "ID" },
+        {
+          field: "barcode",
+          body: "FormattedBarcode",
+          header: "",
+          label: "Barcode",
+          icon: ["fa", "barcode"],
+        },
         { field: "name", header: "Name", label: "Name" },
         { field: "chemform", header: "Formula", body: "ChemicalFormula", label: "Formula" },
         { field: "date", header: "Date", label: "Date" },
-        { field: "chemical_purity", header: "Purity", label: "Purity" },
+        { field: "location", header: "Location", label: "Location", filter: true },
         {
           field: "blocks",
           header: "",
           body: "BlocksIconCounter",
           icon: ["fa", "cubes"],
           label: "Block",
+          filter: true,
         },
         {
           field: "nfiles",
diff --git a/webapp/src/components/SynthesisInformation.vue b/webapp/src/components/SynthesisInformation.vue
index e33fa4056..0fff60d6c 100644
--- a/webapp/src/components/SynthesisInformation.vue
+++ b/webapp/src/components/SynthesisInformation.vue
@@ -76,7 +76,7 @@ export default {
     },
   },
   mounted() {
-    this.selectShown = new Array(this.constituents.length).fill(false);
+    this.selectShown = new Array((this.constituents || []).length).fill(false);
     // Auto-collapsed when initialised empty
     this.isExpanded =
       (this.constituents && this.constituents.length > 0) ||
diff --git a/webapp/src/main.js b/webapp/src/main.js
index 2030a013b..a6cdcca85 100644
--- a/webapp/src/main.js
+++ b/webapp/src/main.js
@@ -10,6 +10,7 @@ import router from "./router";
 
 import { library } from "@fortawesome/fontawesome-svg-core";
 import {
+  faBarcode,
   faSave,
   faCloudUploadAlt,
   faPen,
@@ -52,6 +53,7 @@ import { faPlusSquare } from "@fortawesome/free-regular-svg-icons";
 import { faGithub, faOrcid } from "@fortawesome/free-brands-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 library.add(
+  faBarcode,
   faSave,
   faPen,
   faCloudUploadAlt,


Reviews:
user: login: ml-evs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T11:29:24Z
user: login: BenjaminCharmes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:37:47Z
user: login: BenjaminCharmes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:42:53Z
user: login: BenjaminCharmes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:45:02Z
user: login: BenjaminCharmes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:55:58Z
user: login: BenjaminCharmes
body: LGTM 👍 Everything is working perfectly. 
Just a few minor comments.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:57:00Z
user: login: ml-evs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T16:40:37Z

Review comments:
diff_hunk: @@ -31,6 +18,7 @@ export default {
           filter: true,
           label: "ID",
         },
+        // { field: "refcode", header: "Refcode", body: "FormattedRefcode", filter: true },
path: webapp/src/components/SampleTable.vue
user: login: ml-evs
body: ```suggestion
```
created_at: 2025-05-07T11:29:03Z
author_association: MEMBER
diff_hunk: @@ -17,6 +16,14 @@ export default {
     return {
       startingMaterialColumn: [
         { field: "item_id", header: "ID", body: "FormattedItemName", filter: true, label: "ID" },
+        {
+          field: "barcode",
+          body: "FormattedBarcode",
+          header: "",
+          label: "Barcode",
+          icon: ["fa", "barcode"],
+        },
+        // { field: "refcode", header: "Refcode", body: "FormattedRefcode" },
path: webapp/src/components/StartingMaterialTable.vue
user: login: ml-evs
body: ```suggestion
```
created_at: 2025-05-07T11:29:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,56 @@
+<template>
+  <span v-if="barcode">
+    <font-awesome-icon
+      v-if="enableBarcode"
+      icon="barcode"
+      class="badge clickable"
+      title="Click to show Barcode for this item"
+      aria-label="Click to show Barcode for this item"
+      @click="BarcodeModalOpen = true"
+    />
+    <font-awesome-icon v-else icon="barcode" class="badge" />
+    <span class="badge formatted-barcode" :style="{ backgroundColor: badgeColor }">
+      {{ barcode }}
+    </span>
+    <div v-if="enableBarcode">
+      <BarcodeModal v-model="BarcodeModalOpen" :barcode="barcode" />
+    </div>
+  </span>
+</template>
+
+<script>
+import BarcodeModal from "@/components/BarcodeModal.vue";
+
+export default {
+  components: {
+    BarcodeModal,
+  },
+  props: {
+    barcode: {
+      type: String,
+      required: true,
+    },
+    enableBarcode: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  emits: ["itemIdClicked"],
path: webapp/src/components/FormattedBarcode.vue
user: login: BenjaminCharmes
body: ```suggestion
```

Not sure it's doing anything here.
created_at: 2025-05-07T14:37:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+<template>
+  <form class="modal-enclosure" data-testid="barcode-form">
+    <Modal
+      :model-value="modelValue"
+      :disable-submit="true"
+      @update:model-value="$emit('update:modelValue', $event)"
+    >
+      <template #header>Barcode</template>
+      <template #body>
+        <div class="form-row">
+          <div ref="barcode" class="form-group mx-auto barcode-display" data-testid="barcode">
+            {{ barcode }}
+          </div>
+        </div>
+      </template>
+      <template #footer> </template>
path: webapp/src/components/BarcodeModal.vue
user: login: BenjaminCharmes
body: ```suggestion
```

Don't think it's useful if empty
created_at: 2025-05-07T14:42:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+<template>
+  <form class="modal-enclosure" data-testid="barcode-form">
+    <Modal
+      :model-value="modelValue"
+      :disable-submit="true"
path: webapp/src/components/BarcodeModal.vue
user: login: BenjaminCharmes
body: Maybe we could add something inside the Modal component to not display "Submit" here (And not just disable it).
created_at: 2025-05-07T14:45:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+<template>
+  <span v-if="barcode">
+    <font-awesome-icon
+      v-if="enableBarcode"
+      icon="barcode"
+      class="badge clickable"
+      title="Click to show Barcode for this item"
+      aria-label="Click to show Barcode for this item"
+      @click="BarcodeModalOpen = true"
+    />
+    <font-awesome-icon v-else icon="barcode" class="badge" />
+    <span class="badge formatted-barcode" :style="{ backgroundColor: badgeColor }">
path: webapp/src/components/FormattedBarcode.vue
user: login: BenjaminCharmes
body: ```suggestion
    <span class="badge" :style="{ backgroundColor: badgeColor }">
```

Not used. Is it intended that the barcode icon and the span are not vertically aligned?
created_at: 2025-05-07T14:55:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+<template>
+  <form class="modal-enclosure" data-testid="barcode-form">
+    <Modal
+      :model-value="modelValue"
+      :disable-submit="true"
path: webapp/src/components/BarcodeModal.vue
user: login: ml-evs
body: Yeah, this is what I was trying to do with the blank footer, but alas :(
created_at: 2025-05-07T16:40:37Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-01T13:54:37Z
body: ## [Codecov](https://app.codecov.io/gh/datalab-org/datalab/pull/1163?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=datalab-org) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 71.22%. Comparing base ([`20617b8`](https://app.codecov.io/gh/datalab-org/datalab/commit/20617b8917167414470e24a59da3002c5d83c281?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=datalab-org)) to head ([`48e58ff`](https://app.codecov.io/gh/datalab-org/datalab/commit/48e58ff7ce67df358983c4726d545fe424ee9e15?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=datalab-org)).
:warning: Report is 97 commits behind head on main.

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##             main    #1163      +/-   ##
==========================================
+ Coverage   71.19%   71.22%   +0.02%     
==========================================
  Files          64       64              
  Lines        4236     4240       +4     
==========================================
+ Hits         3016     3020       +4     
  Misses       1220     1220              
```

| [Files with missing lines](https://app.codecov.io/gh/datalab-org/datalab/pull/1163?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=datalab-org) | Coverage Δ | |
|---|---|---|
| [pydatalab/src/pydatalab/routes/v0\_1/items.py](https://app.codecov.io/gh/datalab-org/datalab/pull/1163?src=pr&el=tree&filepath=pydatalab%2Fsrc%2Fpydatalab%2Froutes%2Fv0_1%2Fitems.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=datalab-org#diff-cHlkYXRhbGFiL3NyYy9weWRhdGFsYWIvcm91dGVzL3YwXzEvaXRlbXMucHk=) | `83.59% <100.00%> (+0.20%)` | :arrow_up: |
</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: cypress[bot]
created_at: 2025-05-01T14:06:57Z
body: 
<h3>
  <a href="https://cloud.cypress.io/projects/4kqx5i?utm_source=github&utm_medium=PASSED&utm_campaign=project">datalab</a>&nbsp;
  <img src="https://assets.cypress.io/github-pr-comment-icons/chevron.svg" alt="" height="14" width="14" />&nbsp;
  <a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/overview?utm_source=github&utm_medium=PASSED&utm_campaign=run%20number">Run #3271</a>
</h3>

<details>
  <summary>
    <strong>Run Properties:</strong>&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/overview?utm_source=github&utm_medium=PASSED&utm_campaign=run%20number">#3271</a></code> &nbsp;&bull;&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/datalab-org/datalab/commit/2dad2b768009d31cbcfc07035c209343d7b781f8">2dad2b7680 <span style="cursor:help;" title="Your CI provider generated this commit by merging this branch into the target branch.">ℹ️</span></a>: Merge 48e58ff7ce67df358983c4726d545fe424ee9e15 into 20617b8917167414470e24a59da3...</code>
  </summary><br />

  <table width="744">
    <tbody>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Project</strong>
        </td>
        <td width="520">
          <code><a href="https://cloud.cypress.io/projects/4kqx5i?utm_source=github&utm_medium=PASSED&utm_campaign=project">datalab</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Branch Review</strong>
        </th>
        <td>
          <code><a href="https://cloud.cypress.io/projects/4kqx5i/branches/ml-evs%2Finventory-qol/review?utm_source=github&utm_medium=PASSED&utm_campaign=review%20changes&changeRequest=1163">ml-evs/inventory-qol</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run status</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/overview?utm_source=github&utm_medium=PASSED&utm_campaign=run%20number">#3271</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run duration</strong></td>
        <td><code>07m 36s</code></td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Commit</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/datalab-org/datalab/commit/2dad2b768009d31cbcfc07035c209343d7b781f8">2dad2b7680 <span style="cursor:help;" title="Your CI provider generated this commit by merging this branch into the target branch.">ℹ️</span></a>: Merge 48e58ff7ce67df358983c4726d545fe424ee9e15 into 20617b8917167414470e24a59da3...</code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Committer</strong></td>
        <td><code>Matthew Evans</code></td>
      </tr>
    </tbody>
    <tfoot>
      <tr />
      <tr height="48">
        <td colspan="2">
          <a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/properties?utm_source=github&utm_medium=PASSED&utm_campaign=properties">View all properties for this run ↗︎</a>
        </td>
      </tr>
    </tfoot>
  </table>
  <hr />
</details>

<table width="744">
  <thead>
    <tr />
    <tr height="48">
      <th colspan="2" align="left" width="744">Test results</th>
    </tr>
  </thead>
  <tbody>
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/failed.svg" alt="Tests that failed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/overview?reviewViewBy=FAILED&utm_source=github&utm_medium=PASSED&utm_campaign=failed%20tests">
          <strong>Failures</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/flaky-v3.svg" alt="Tests that were flaky" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/overview?reviewViewBy=FLAKY&utm_source=github&utm_medium=PASSED&utm_campaign=flaky%20tests">
          <strong>Flaky</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/pending.svg" alt="Tests that did not run due to a developer annotating a test with .skip" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/test-results?utm_source=github&utm_medium=PASSED&utm_campaign=pending%20tests&statuses=%5B%7B%22value%22%3A%22PENDING%22%2C%22label%22%3A%22PENDING%22%7D%5D">
          <strong>Pending</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/skipped-v3.svg" alt="Tests that did not run due to a failure in a mocha hook" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/test-results?utm_source=github&utm_medium=PASSED&utm_campaign=skipped%20tests&statuses=%5B%7B%22value%22%3A%22SKIPPED%22%2C%22label%22%3A%22SKIPPED%22%7D%5D">
          <strong>Skipped</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="Tests that passed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/4kqx5i/runs/3271/overview?utm_source=github&utm_medium=PASSED&utm_campaign=passed%20tests">
          <strong>Passing</strong>
        </a>
      </td>
      <td>
        <code><strong>504</strong></code>
      </td>
    </tr><tfoot>
    <tr height="48"><td colspan="2"><a href="https://cloud.cypress.io/projects/4kqx5i/branches/ml-evs%2Finventory-qol/review?utm_source=github&utm_medium=PASSED&utm_campaign=review%20changes&changeRequest=1163">View all changes introduced in this branch ↗︎</a></td></tr></tfoot>
  </tbody>
</table>






author_association: NONE
user: login: ml-evs
created_at: 2025-05-02T11:56:00Z
body: This is semi-blocked by #1124 (in that I want to use it to set the barcode to be a non-default column that user's can remember)
author_association: MEMBER
user: login: ml-evs
created_at: 2025-05-07T16:49:35Z
body: > LGTM 👍 Everything is working perfectly. Just a few minor comments.

Thanks! All fixed so I'll merge when the tests pass.
author_association: MEMBER