@bitpoolos/edge-bacnet 1.6.2 → 1.6.4

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/bacnet_read.html CHANGED
@@ -62,6 +62,7 @@
62
62
  progressBarValue: ref(),
63
63
  rightClickedDevice: ref(),
64
64
  rightClickedPoint: ref(),
65
+ rightClickedMstpFolder: ref(),
65
66
  showDeviceNameDialog: ref(false),
66
67
  showPointNameDialog: ref(false),
67
68
  deviceDisplayNameValue: ref(),
@@ -130,10 +131,7 @@
130
131
  getData(initial) {
131
132
  let app = this;
132
133
  this.nodeService.getNetworkData().then(function (result) {
133
- //remove after below fixed
134
134
  app.devices = result.renderList;
135
- //end remove
136
-
137
135
  app.deviceList = result.deviceList;
138
136
  app.pointList = result.pointList;
139
137
  app.pollFrequency = parseInt(result.pollFrequency);
@@ -288,7 +286,13 @@
288
286
  newReadParent = JSON.parse(JSON.stringify(parentDevice));
289
287
  }
290
288
  newReadParent.children[0].children = [];
291
- newReadParent.children[0].children.push(slotProps.node);
289
+
290
+ // Create a copy of the point with preserved display name
291
+ let pointCopy = JSON.parse(JSON.stringify(slotProps.node));
292
+ // Ensure display name is preserved from the original point
293
+ pointCopy.label = slotProps.node.label;
294
+ newReadParent.children[0].children.push(pointCopy);
295
+
292
296
  while (newReadParent.children.length > 1) {
293
297
  newReadParent.children.forEach(function (child, index) {
294
298
  if (child.label.includes("MSTP")) {
@@ -307,7 +311,10 @@
307
311
  (ele) => ele.pointName == slotProps.node.pointName
308
312
  );
309
313
  if (pointIndex == -1) {
310
- this.readDevices[foundDeviceIndex].children[0].children.push(slotProps.node);
314
+ // Create a copy of the point with preserved display name
315
+ let pointCopy = JSON.parse(JSON.stringify(slotProps.node));
316
+ pointCopy.label = slotProps.node.label;
317
+ this.readDevices[foundDeviceIndex].children[0].children.push(pointCopy);
311
318
  }
312
319
  }
313
320
 
@@ -321,6 +328,10 @@
321
328
  }
322
329
 
323
330
  let point = this.pointList[key][slotProps.node.pointName];
331
+ // Preserve any existing display name when adding to pointsToRead
332
+ if (slotProps.node.label !== slotProps.node.pointName) {
333
+ point.displayName = slotProps.node.label;
334
+ }
324
335
  this.pointsToRead[key][point.objectName] = point;
325
336
 
326
337
  //force a deploy state
@@ -566,6 +577,27 @@
566
577
  pointMenu.classList.remove("pointAddedToRead");
567
578
  }
568
579
  },
580
+ // NEW: Handle right-click on MSTP network folders
581
+ onMstpFolderRightClick(slotProps, event) {
582
+ let app = this;
583
+ app.rightClickedMstpFolder = slotProps;
584
+ event.preventDefault();
585
+ event.stopPropagation();
586
+
587
+ // Hide other context menus first
588
+ const menu = document.querySelector(".context-menu");
589
+ const pointMenu = document.querySelector(".point-context-menu");
590
+ if (menu) menu.style.display = "none";
591
+ if (pointMenu) pointMenu.style.display = "none";
592
+
593
+ const mstpFolderMenu = document.querySelector(".mstp-folder-context-menu");
594
+
595
+ if (mstpFolderMenu) {
596
+ mstpFolderMenu.style.setProperty("--mouse-x", event.clientX + "px");
597
+ mstpFolderMenu.style.setProperty("--mouse-y", event.clientY + "px");
598
+ mstpFolderMenu.style.display = "block";
599
+ }
600
+ },
569
601
  handleContextMenuClick(type) {
570
602
  let app = this;
571
603
  switch (type) {
@@ -589,6 +621,17 @@
589
621
  break;
590
622
  }
591
623
  },
624
+ // NEW: Handle context menu clicks for MSTP folders
625
+ handleMstpFolderContextMenuClick(type) {
626
+ let app = this;
627
+ switch (type) {
628
+ case "updateAllDevices":
629
+ app.updateAllMstpDevices(app.rightClickedMstpFolder);
630
+ break;
631
+ default:
632
+ break;
633
+ }
634
+ },
592
635
  handlePointContextMenuClick(type) {
593
636
  let app = this;
594
637
  switch (type) {
@@ -603,6 +646,43 @@
603
646
  break;
604
647
  }
605
648
  },
649
+ // NEW: Update all devices within an MSTP network folder
650
+ updateAllMstpDevices(slotProps) {
651
+ let app = this;
652
+
653
+ if (!slotProps || !slotProps.node || !slotProps.node.children) {
654
+ return;
655
+ }
656
+
657
+ const mstpDevices = slotProps.node.children;
658
+ let updatedCount = 0;
659
+
660
+ // Iterate through all MSTP devices in the folder
661
+ mstpDevices.forEach(function (mstpDevice) {
662
+ // Find the device in the device list
663
+ let device = app.getDeviceFromDeviceList(mstpDevice.ipAddr, mstpDevice.deviceId);
664
+ if (device) {
665
+ app.nodeService
666
+ .updatePointsForDevice(device)
667
+ .then(function (result) {
668
+ updatedCount++;
669
+ })
670
+ .catch(function (error) {
671
+ // Handle error silently
672
+ });
673
+ }
674
+ });
675
+
676
+ // Show user feedback
677
+ if (mstpDevices.length > 0) {
678
+ app.$toast?.add({
679
+ severity: "info",
680
+ summary: "Update Started",
681
+ detail: `Updating points for ${mstpDevices.length} devices in ${slotProps.node.label}`,
682
+ life: 3000,
683
+ });
684
+ }
685
+ },
606
686
  purgeDevice(slotProps) {
607
687
  let app = this;
608
688
  let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
@@ -660,13 +740,94 @@
660
740
 
661
741
  app.nodeService.setPointDisplayName(deviceKey, pointName, pointDisplayName).then(function (result) {
662
742
  if (result) {
663
- slotProps.node.label = pointDisplayName;
743
+ // Update the display name across all UI representations
744
+ app.syncPointDisplayNameAcrossUI(deviceKey, pointName, pointDisplayName);
745
+ app.updatePointsToReadDisplayName(deviceKey, pointName, pointDisplayName);
664
746
  }
665
747
  });
666
748
  }
667
749
 
668
750
  app.showPointNameDialog = false;
669
751
  },
752
+ // NEW: Centralized method to sync display names across all UI trees
753
+ syncPointDisplayNameAcrossUI(deviceKey, pointName, newDisplayName) {
754
+ let app = this;
755
+
756
+ // Update in main devices tree
757
+ app.updatePointDisplayNameInDevicesTree(deviceKey, pointName, newDisplayName);
758
+
759
+ // Update in read devices tree
760
+ app.updatePointDisplayNameInReadDevicesTree(deviceKey, pointName, newDisplayName);
761
+
762
+ // Force UI update
763
+ app.$forceUpdate();
764
+ },
765
+ // NEW: Update display name in main devices tree
766
+ updatePointDisplayNameInDevicesTree(deviceKey, pointName, newDisplayName) {
767
+ let app = this;
768
+ let [ipAddress, deviceId] = deviceKey.split("-");
769
+
770
+ // Find the device in main tree
771
+ let deviceIndex = app.devices
772
+ ? app.devices.findIndex((device) => device.ipAddr === ipAddress && device.deviceId.toString() === deviceId)
773
+ : -1;
774
+
775
+ if (deviceIndex !== -1) {
776
+ // Update direct device points
777
+ let pointIndex = app.devices[deviceIndex].children[0].children.findIndex((point) => point.pointName === pointName);
778
+ if (pointIndex !== -1) {
779
+ app.devices[deviceIndex].children[0].children[pointIndex].label = newDisplayName;
780
+ }
781
+
782
+ // Update MSTP device points if applicable
783
+ app.devices[deviceIndex].children.forEach((child) => {
784
+ if (child.label && child.label.includes("MSTP") && child.children) {
785
+ child.children.forEach((mstpDevice) => {
786
+ if (mstpDevice.deviceId.toString() === deviceId) {
787
+ let mstpPointIndex = mstpDevice.children[0].children.findIndex((point) => point.pointName === pointName);
788
+ if (mstpPointIndex !== -1) {
789
+ mstpDevice.children[0].children[mstpPointIndex].label = newDisplayName;
790
+ }
791
+ }
792
+ });
793
+ }
794
+ });
795
+ }
796
+ },
797
+ // NEW: Update display name in read devices tree
798
+ updatePointDisplayNameInReadDevicesTree(deviceKey, pointName, newDisplayName) {
799
+ let app = this;
800
+ let [ipAddress, deviceId] = deviceKey.split("-");
801
+
802
+ if (!app.readDevices) return;
803
+
804
+ // Find the device in read devices tree
805
+ let readDeviceIndex = app.readDevices.findIndex((device) => device.deviceId.toString() === deviceId);
806
+
807
+ if (readDeviceIndex !== -1) {
808
+ let pointIndex = app.readDevices[readDeviceIndex].children[0].children.findIndex(
809
+ (point) => point.pointName === pointName
810
+ );
811
+ if (pointIndex !== -1) {
812
+ app.readDevices[readDeviceIndex].children[0].children[pointIndex].label = newDisplayName;
813
+ }
814
+ }
815
+ },
816
+ // NEW: Update display name in pointsToRead data structure
817
+ updatePointsToReadDisplayName(deviceKey, pointName, newDisplayName) {
818
+ let app = this;
819
+
820
+ if (app.pointsToRead && app.pointsToRead[deviceKey]) {
821
+ // Find the point by objectName (since pointsToRead uses objectName as key)
822
+ for (let objectName in app.pointsToRead[deviceKey]) {
823
+ let point = app.pointsToRead[deviceKey][objectName];
824
+ if (point && point.objectName === pointName) {
825
+ point.displayName = newDisplayName;
826
+ break;
827
+ }
828
+ }
829
+ }
830
+ },
670
831
  getDeviceFromDeviceList(ip, id) {
671
832
  let app = this;
672
833
  let device = app.deviceList.find((ele) => {
@@ -896,7 +1057,30 @@
896
1057
  app.$forceUpdate();
897
1058
  },
898
1059
  refreshReadListTree() {
899
- this.addToReadDevices(this.pointsToRead);
1060
+ // Enhanced refresh that maintains display names
1061
+ let app = this;
1062
+
1063
+ // First apply any saved display names to the current pointsToRead
1064
+ app.applyStoredDisplayNames();
1065
+
1066
+ // Then rebuild the read list tree
1067
+ app.addToReadDevices(app.pointsToRead);
1068
+ },
1069
+ // NEW: Apply stored display names from backend to current data
1070
+ applyStoredDisplayNames() {
1071
+ let app = this;
1072
+
1073
+ if (!app.pointsToRead) return;
1074
+
1075
+ // Send current pointsToRead to backend to apply any stored display names
1076
+ let msg = { applyDisplayNames: true };
1077
+ // This will be handled by the backend node to apply stored display names
1078
+ app.nodeService.applyDisplayNames(app.pointsToRead).then(function (result) {
1079
+ if (result) {
1080
+ // Refresh the main tree with updated display names
1081
+ app.getData();
1082
+ }
1083
+ });
900
1084
  },
901
1085
  calculateMstpCount(slotProps) {
902
1086
  let count = 0;
@@ -970,6 +1154,13 @@
970
1154
  pointMenu.style.display = "none";
971
1155
  });
972
1156
 
1157
+ var mstpFolderMenu = document.querySelector(".mstp-folder-context-menu");
1158
+ window.addEventListener("click", (event) => {
1159
+ if (menu) menu.style.display = "none";
1160
+ if (pointMenu) pointMenu.style.display = "none";
1161
+ if (mstpFolderMenu) mstpFolderMenu.style.display = "none";
1162
+ });
1163
+
973
1164
  function handleCheckboxClick() {
974
1165
  if (this.id == "node-input-object_property_simplePayload") {
975
1166
  document.getElementById("node-input-object_property_fullObject").checked = false;
@@ -1113,6 +1304,18 @@
1113
1304
  </ul>
1114
1305
  <!-- End Point Context Menu -->
1115
1306
 
1307
+ <!-- Start MSTP Folder Context Menu -->
1308
+ <ul
1309
+ class="red-ui-menu red-ui-menu-dropdown red-ui-menu-dropdown-direction-right red-ui-menu-dropdown-noicons red-ui-menu-dropdown-submenus mstp-folder-context-menu">
1310
+ <li class="context-menu-item" @click="handleMstpFolderContextMenuClick('updateAllDevices')">
1311
+ <a class="red-ui-menu-label">
1312
+ <i class="pi pi-refresh context-menu-icon"></i>
1313
+ <span class="context-menu-item-text">Update All Devices</span>
1314
+ </a>
1315
+ </li>
1316
+ </ul>
1317
+ <!-- End MSTP Folder Context Menu -->
1318
+
1116
1319
  <!--
1117
1320
  *
1118
1321
  *
@@ -1233,6 +1436,19 @@
1233
1436
  </div>
1234
1437
  </template>
1235
1438
 
1439
+ <template #mstpfolder="slotProps">
1440
+ <div @contextmenu="onMstpFolderRightClick(slotProps, $event)" class="p-treenode-label">
1441
+ <div class="deviceLabelParent">
1442
+ <b class="mstpLabel">
1443
+ <span>{{slotProps.node.label}}</span>
1444
+ <span class="mstpDeviceCount">
1445
+ &nbsp; {{slotProps.node.children ? slotProps.node.children.length : 0}} &nbsp;
1446
+ </span>
1447
+ </b>
1448
+ </div>
1449
+ </div>
1450
+ </template>
1451
+
1236
1452
  <template #point="slotProps" v-model:class="pointContent">
1237
1453
  <div @contextmenu="onPointRightClick(slotProps, $event)">
1238
1454
  <b class="pointLabel">{{slotProps.node.label}}</b>
package/bacnet_read.js CHANGED
@@ -99,7 +99,19 @@ module.exports = function (RED) {
99
99
  useDeviceName = node.useDeviceName;
100
100
  }
101
101
 
102
- let readConfig = new ReadCommandConfig(node.pointsToRead, node.object_props, node.roundDecimal);
102
+ // Ensure pointsToRead has the latest display names before processing
103
+ let pointsToReadWithDisplayNames = JSON.parse(JSON.stringify(node.pointsToRead));
104
+
105
+ // Apply any stored display names from the backend data model
106
+ for (let deviceKey in pointsToReadWithDisplayNames) {
107
+ for (let pointKey in pointsToReadWithDisplayNames[deviceKey]) {
108
+ let point = pointsToReadWithDisplayNames[deviceKey][pointKey];
109
+ // The display name should already be preserved in the point object
110
+ // from the setPointDisplayName operations and addPointClicked enhancements
111
+ }
112
+ }
113
+
114
+ let readConfig = new ReadCommandConfig(pointsToReadWithDisplayNames, node.object_props, node.roundDecimal);
103
115
 
104
116
  let output = {
105
117
  type: "Read",
package/common.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const { randomUUID } = require("crypto");
6
6
  const os = require("os");
7
+ const path = require("path");
7
8
  const baEnum = require("./resources/node-bacstack-ts/dist/index.js").enum;
8
9
  const fs = require("fs");
9
10
  const fs2 = require("fs").promises;
@@ -189,27 +190,16 @@ const roundDecimalPlaces = function (value, decimals) {
189
190
  return value;
190
191
  };
191
192
 
192
- // STORE CONFIG FUNCTION ==========================================================
193
- //
194
- // ================================================================================
195
-
196
- /*
197
-
198
- async function Store_Config(data) {
199
- try {
200
- await fs.writeFile("edge-bacnet-datastore.cfg", data, { encoding: "utf8", flag: "w" }, (err) => {
201
- if (err) {
202
- console.log("Store_Config writeFile error: ", err);
203
- }
204
- });
205
- } catch (e) {
206
- //do nothing
193
+ const getStoragePath = (fileName) => {
194
+ const storagePath = process.env.BACNET_STORAGE_PATH;
195
+ if (storagePath) {
196
+ if (!fs.existsSync(storagePath)) {
197
+ fs.mkdirSync(storagePath, { recursive: true });
198
+ }
199
+ return path.join(storagePath, fileName);
207
200
  }
208
- }
209
-
210
- */
211
-
212
- // refactor:
201
+ return fileName;
202
+ };
213
203
 
214
204
  let storeQueue = [];
215
205
  let isStoreProcessing = false;
@@ -235,9 +225,9 @@ async function queueConfigStore(data) {
235
225
  }
236
226
 
237
227
  async function Store_Config(data) {
238
- const mainFile = "edge-bacnet-datastore.cfg";
239
- const tempFile = "edge-bacnet-datastore.cfg.tmp";
240
- const backupFile = "edge-bacnet-datastore.cfg.bak";
228
+ const mainFile = getStoragePath("edge-bacnet-datastore.cfg");
229
+ const tempFile = getStoragePath("edge-bacnet-datastore.cfg.tmp");
230
+ const backupFile = getStoragePath("edge-bacnet-datastore.cfg.bak");
241
231
 
242
232
  try {
243
233
  // First validate the JSON to ensure it's valid before writing
@@ -295,54 +285,10 @@ async function Store_Config(data) {
295
285
  }
296
286
  }
297
287
 
298
- // READ CONFIG SYNC FUNCTION ======================================================
299
- //
300
- // ================================================================================
301
-
302
- function Read_Config_Sync() {
303
- const mainFile = "edge-bacnet-datastore.cfg";
304
- const backupFile = "edge-bacnet-datastore.cfg.bak";
305
- const defaultData = "{}";
306
-
307
- try {
308
- // Try to read the main file
309
- let data = fsSync.readFileSync(mainFile, { encoding: "utf8" });
310
-
311
- // Validate JSON
312
- try {
313
- JSON.parse(data);
314
- return data;
315
- } catch (jsonError) {
316
- console.error("Main file contains invalid JSON, attempting backup recovery");
317
-
318
- // Try to read backup file
319
- try {
320
- const backupData = fsSync.readFileSync(backupFile, { encoding: "utf8" });
321
- JSON.parse(backupData); // Validate backup JSON
322
-
323
- // Restore from backup
324
- fsSync.copyFileSync(backupFile, mainFile);
325
- console.log("Successfully restored from backup file");
326
- return backupData;
327
- } catch (backupError) {
328
- console.error("Backup recovery failed, creating new file");
329
- fsSync.writeFileSync(mainFile, defaultData, { encoding: "utf8" });
330
- return defaultData;
331
- }
332
- }
333
- } catch (error) {
334
- console.error("Error reading config:", error);
335
- fsSync.writeFileSync(mainFile, defaultData, { encoding: "utf8" });
336
- return defaultData;
337
- }
338
- }
339
-
340
- // refactor:
341
-
342
288
  async function Read_Config_Async() {
343
289
  // todo rename function, not using sync
344
- const mainFile = "edge-bacnet-datastore.cfg";
345
- const backupFile = "edge-bacnet-datastore.cfg.bak";
290
+ const mainFile = getStoragePath("edge-bacnet-datastore.cfg");
291
+ const backupFile = getStoragePath("edge-bacnet-datastore.cfg.bak");
346
292
  const defaultData = "{}";
347
293
 
348
294
  try {
@@ -366,15 +312,11 @@ async function Read_Config_Async() {
366
312
  await fs.copyFile(backupFile, mainFile);
367
313
  console.log("Successfully restored from backup file");
368
314
 
369
- console.log("log2");
370
-
371
315
  return backupData;
372
316
  } catch (backupError) {
373
317
  console.error("Backup recovery failed, creating new file");
374
318
  await Store_Config(defaultData);
375
319
 
376
- console.log("log3");
377
-
378
320
  return defaultData;
379
321
  }
380
322
  }
@@ -382,8 +324,6 @@ async function Read_Config_Async() {
382
324
  console.error("Error reading config:", error);
383
325
  await Store_Config(defaultData);
384
326
 
385
- console.log("log4");
386
-
387
327
  return defaultData;
388
328
  }
389
329
  }
@@ -393,7 +333,7 @@ async function Read_Config_Async() {
393
333
  // ================================================================================
394
334
  async function Store_Config_Server(data) {
395
335
  try {
396
- await fs.writeFile("edge-bacnet-server-datastore.cfg", data, (err) => {
336
+ await fs.writeFile(getStoragePath("edge-bacnet-server-datastore.cfg"), data, (err) => {
397
337
  if (err) {
398
338
  //console.log("Store_Config_Server writeFile error: ", err);
399
339
  }
@@ -407,7 +347,7 @@ async function Store_Config_Server(data) {
407
347
  function Read_Config_Sync_Server() {
408
348
  var data = "{}";
409
349
  try {
410
- data = fs.readFileSync("edge-bacnet-server-datastore.cfg", { encoding: "utf8", flag: "r" });
350
+ data = fs.readFileSync(getStoragePath("edge-bacnet-server-datastore.cfg"), { encoding: "utf8", flag: "r" });
411
351
  } catch (err) {
412
352
  if (err.errno == -4058) {
413
353
  data = "{}";
@@ -485,7 +425,6 @@ module.exports = {
485
425
  roundDecimalPlaces,
486
426
  queueConfigStore,
487
427
  Store_Config,
488
- Read_Config_Sync,
489
428
  Read_Config_Async,
490
429
  Store_Config_Server,
491
430
  Read_Config_Sync_Server,
package/inspector.html CHANGED
@@ -186,7 +186,11 @@
186
186
  </div>
187
187
  </template>
188
188
  <p-column v-for="col in visibleColumns" :key="col.field" :field="col.field" :header="col.header" sortable
189
- filter></p-column>
189
+ filter>
190
+ <template #body="slotProps" v-if="col.field === 'objectType'">
191
+ {{ getObjectTypeString(slotProps.data.objectType) }}
192
+ </template>
193
+ </p-column>
190
194
  <template #paginatorstart>
191
195
 
192
196
  </template>
@@ -331,6 +335,36 @@
331
335
  },
332
336
  },
333
337
  methods: {
338
+ getObjectTypeString(objectType) {
339
+ switch (objectType) {
340
+ case 0:
341
+ return "Analog Input";
342
+ case 1:
343
+ return "Analog Output";
344
+ case 2:
345
+ return "Analog Value";
346
+ case 3:
347
+ return "Binary Input";
348
+ case 4:
349
+ return "Binary Output";
350
+ case 5:
351
+ return "Binary Value";
352
+ case 8:
353
+ return "Device";
354
+ case 10:
355
+ return "File";
356
+ case 13:
357
+ return "Multistate Input";
358
+ case 14:
359
+ return "Multistate Output";
360
+ case 19:
361
+ return "Multistate Value";
362
+ case 40:
363
+ return "Character String";
364
+ default:
365
+ return "";
366
+ }
367
+ },
334
368
  statusItemClicked(e) {
335
369
  // Get the filter category from the clicked item's text content
336
370
  const clickedItem = e.currentTarget;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpoolos/edge-bacnet",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "A bacnet gateway for node-red",
5
5
  "dependencies": {
6
6
  "@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
@@ -54,4 +54,4 @@
54
54
  "type": "github",
55
55
  "url": "git+https://github.com/bitpool/edge-bacnet.git"
56
56
  }
57
- }
57
+ }
@@ -339,7 +339,9 @@
339
339
  cursor: not-allowed;
340
340
  }
341
341
  }
342
-
342
+ .mstpLabel {
343
+ font-weight: 400;
344
+ }
343
345
  .point-context-menu {
344
346
  --mouse-x: 0;
345
347
  --mouse-y: 0;
@@ -352,6 +354,18 @@
352
354
  transform: translateX(min(var(--mouse-x), calc(100vw - 100%))) translateY(min(var(--mouse-y), calc(100vh - 100%)));
353
355
  z-index: 10;
354
356
  }
357
+ .mstp-folder-context-menu {
358
+ --mouse-x: 0;
359
+ --mouse-y: 0;
360
+ display: none;
361
+ position: fixed;
362
+ margin: 0;
363
+ left: 0;
364
+ top: 0;
365
+ /* The following line is responsible for all the magic */
366
+ transform: translateX(min(var(--mouse-x), calc(100vw - 100%))) translateY(min(var(--mouse-y), calc(100vh - 100%)));
367
+ z-index: 10;
368
+ }
355
369
  .context-menu {
356
370
  --mouse-x: 0;
357
371
  --mouse-y: 0;
@@ -765,4 +779,17 @@
765
779
  .p-confirm-dialog-accept > .p-button-label,
766
780
  .bacnetServerRebuildSchedule_clearButton > .p-button-label {
767
781
  color: white;
782
+ }
783
+
784
+ .deviceFolderLabel {
785
+ display: flex;
786
+ align-items: center;
787
+ color: #333;
788
+ font-weight: 600;
789
+ }
790
+
791
+ .deviceFolderLabel:hover {
792
+ background-color: #f8f9fa;
793
+ border-radius: 4px;
794
+ padding: 2px 4px;
768
795
  }