@vitormnm/node-red-simple-opcua 1.5.0 → 1.6.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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  (function () {
3
- var editorState = { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
3
+ var editorState = { objects: [], folders: [], objectsTypes: [], enumerations: [], nameSpaces: [] };
4
4
  var expansionState = {};
5
5
  var selectedPath = "";
6
6
  var pendingCreate = null;
@@ -21,11 +21,11 @@
21
21
  function closeAuthModal() { $("#node-input-auth-modal").hide(); syncModalBodyClass(); }
22
22
 
23
23
  function parseTree(rawValue, strict) {
24
- if (!rawValue) return { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
24
+ if (!rawValue) return { objects: [], folders: [], objectsTypes: [], enumerations: [], nameSpaces: [] };
25
25
  if (typeof rawValue === "object") return rawValue;
26
26
  try { var parsed = JSON.parse(rawValue); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed; }
27
27
  catch (error) { if (strict) throw error; }
28
- return { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
28
+ return { objects: [], folders: [], objectsTypes: [], enumerations: [], nameSpaces: [] };
29
29
  }
30
30
 
31
31
  function parseCredentialArray(rawValue) {
@@ -74,9 +74,13 @@
74
74
 
75
75
  function reconcileAuthGroupsFromUsers() {
76
76
  authUsers.forEach(function (user) {
77
- var groupName = String((user && user.group) || "").trim();
78
- if (groupName && authGroups.indexOf(groupName) === -1) {
79
- authGroups.push(groupName);
77
+ if (user && user.group) {
78
+ var groups = String(user.group).split(",").map(function (g) { return g.trim(); }).filter(Boolean);
79
+ groups.forEach(function (groupName) {
80
+ if (authGroups.indexOf(groupName) === -1) {
81
+ authGroups.push(groupName);
82
+ }
83
+ });
80
84
  }
81
85
  });
82
86
  }
@@ -221,6 +225,29 @@
221
225
  };
222
226
  }
223
227
 
228
+ function normalizeEnumerationState(state) {
229
+ state = state || {};
230
+ return {
231
+ value: state.value !== undefined ? Number(state.value) : 0,
232
+ displayName: state.displayName ? String(state.displayName) : ""
233
+ };
234
+ }
235
+
236
+ function normalizeEnumeration(enumeration) {
237
+ enumeration = enumeration || {};
238
+ return {
239
+ name: enumeration.name ? String(enumeration.name) : "",
240
+ description: enumeration.description ? String(enumeration.description) : "",
241
+ displayName: enumeration.displayName ? String(enumeration.displayName) : "",
242
+ nodeId: enumeration.nodeId ? String(enumeration.nodeId) : "",
243
+ namespaceId: normalizeNamespaceId(enumeration.namespaceId),
244
+ accessPermission: normalizeAccessPermissionValues(enumeration.accessPermission || enumeration.accessPermissions),
245
+ enumeration: Array.isArray(enumeration.enumeration)
246
+ ? enumeration.enumeration.map(normalizeEnumerationState)
247
+ : []
248
+ };
249
+ }
250
+
224
251
  function normalizeBranch(branch) {
225
252
  branch = branch || {};
226
253
  return {
@@ -246,6 +273,7 @@
246
273
  objects: Array.isArray(tree.objects) ? tree.objects.map(normalizeBranch) : [],
247
274
  folders: Array.isArray(tree.folders) ? tree.folders.map(normalizeBranch) : [],
248
275
  objectsTypes: Array.isArray(tree.objectsTypes) ? tree.objectsTypes.map(normalizeBranch) : (Array.isArray(tree.objectTypes) ? tree.objectTypes.map(normalizeBranch) : []),
276
+ enumerations: Array.isArray(tree.enumerations) ? tree.enumerations.map(normalizeEnumeration) : (Array.isArray(tree.enumeration) ? tree.enumeration.map(normalizeEnumeration) : []),
249
277
  nameSpaces: Array.isArray(tree.nameSpaces) ? tree.nameSpaces.map(normalizeNamespaceDefinition) : (Array.isArray(tree.namespaces) ? tree.namespaces.map(normalizeNamespaceDefinition) : [])
250
278
  });
251
279
  }
@@ -300,6 +328,7 @@
300
328
  if (collectionToken === "methods") return "Method";
301
329
  if (collectionToken === "alarms") return "Alarm";
302
330
  if (collectionToken === "objectsTypes" || collectionToken === "objectTypes") return "ObjectType";
331
+ if (collectionToken === "enumerations" || collectionToken === "enumeration") return "Enumeration";
303
332
  if (collectionToken === "nameSpaces" || collectionToken === "namespaces") return "Namespace";
304
333
  if (collectionToken === "folders") return "Folder";
305
334
  return "Object";
@@ -332,6 +361,28 @@
332
361
  return "<select id=\"" + id + "\">" + opts + "</select>";
333
362
  }
334
363
 
364
+ function getDefinedEnumerationNames() {
365
+ var names = [];
366
+ (editorState.enumerations || []).forEach(function (e) {
367
+ if (e && e.name) names.push(String(e.name));
368
+ });
369
+ return names;
370
+ }
371
+
372
+ function buildEnumerationSelect(id, currentValue) {
373
+ var names = getDefinedEnumerationNames();
374
+ var cv = String(currentValue || "");
375
+ var opts = "";
376
+ var noneSelected = (cv === "") ? " selected" : "";
377
+ opts += "<option value=\"\"" + noneSelected + ">\u2014 none \u2014</option>";
378
+ names.forEach(function (n) {
379
+ var esc = n.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
380
+ var sel = (n === cv) ? " selected" : "";
381
+ opts += "<option value=\"" + esc + "\"" + sel + ">" + esc + "</option>";
382
+ });
383
+ return "<select id=\"" + id + "\">" + opts + "</select>";
384
+ }
385
+
335
386
  function getAvailableAccessPermissionOptions() {
336
387
  var values = ["public"].concat(authGroups || []);
337
388
  return normalizeAccessPermissionValues(values);
@@ -493,6 +544,7 @@
493
544
  (editorState.folders || []).forEach(function (_, i) { paths.push("folders." + i); });
494
545
  (editorState.objects || []).forEach(function (_, i) { paths.push("objects." + i); });
495
546
  (editorState.objectsTypes || []).forEach(function (_, i) { paths.push("objectsTypes." + i); });
547
+ (editorState.enumerations || []).forEach(function (_, i) { paths.push("enumerations." + i); });
496
548
  (editorState.nameSpaces || []).forEach(function (_, i) { paths.push("nameSpaces." + i); });
497
549
  return paths;
498
550
  }
@@ -520,6 +572,7 @@
520
572
  if (nodeClass === "Object") return "fa-cube";
521
573
  if (nodeClass === "Variable") return "fa-tag";
522
574
  if (nodeClass === "ObjectType") return "fa-cubes";
575
+ if (nodeClass === "Enumeration") return "fa-list-ol";
523
576
  if (nodeClass === "Namespace") return "fa-sitemap";
524
577
  if (nodeClass === "Alarm") return "fa-bell";
525
578
  if (nodeClass === "Method") return "fa-cog";
@@ -626,9 +679,9 @@
626
679
  pendingCreate = {
627
680
  parentPath: path,
628
681
  kind: kind,
629
- name: kind === "variable" ? "newVariable" : kind === "folder" ? "newFolder" : kind === "objecttype" ? "newObjectType" : kind === "alarm" ? "newAlarm" : kind === "method" ? "newMethod" : "newObject",
682
+ name: kind === "variable" ? "newVariable" : kind === "enum-variable" ? "newEnumVariable" : kind === "folder" ? "newFolder" : kind === "objecttype" ? "newObjectType" : kind === "alarm" ? "newAlarm" : kind === "method" ? "newMethod" : "newObject",
630
683
  displayName: "",
631
- dataType: "Int32",
684
+ dataType: kind === "enum-variable" ? (getDefinedEnumerationNames()[0] || "") : "Int32",
632
685
  value: "",
633
686
  access: "readwrite",
634
687
  accessPermission: ["public"],
@@ -654,7 +707,7 @@
654
707
  if (!pendingCreate) return;
655
708
  var parentPath = pendingCreate.parentPath;
656
709
  var kind = pendingCreate.kind;
657
- var branchTargetPath = kind === "variable"
710
+ var branchTargetPath = (kind === "variable" || kind === "enum-variable")
658
711
  ? (parentPath + ".variables")
659
712
  : kind === "folder"
660
713
  ? (parentPath + ".folders")
@@ -667,7 +720,7 @@
667
720
  : (parentPath + ".objects");
668
721
  var target = getAtPath(editorState, branchTargetPath);
669
722
  if (!Array.isArray(target)) return;
670
- if (kind === "variable") {
723
+ if (kind === "variable" || kind === "enum-variable") {
671
724
  target.push(normalizeVariable({
672
725
  name: pendingCreate.name,
673
726
  displayName: pendingCreate.displayName || "",
@@ -725,8 +778,10 @@
725
778
  panel.append('<div class="form-row"><label>Name</label><input type="text" id="opcua-create-name"></div>');
726
779
  panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-create-displayname" placeholder="Leave blank to use browseName"></div>');
727
780
  panel.append('<div class="form-row"><label>accessPermission</label>' + buildAccessPermissionSelect("opcua-create-accesspermission", pendingCreate.accessPermission || ["public"]) + '</div>');
728
- if (pendingCreate.kind === "variable") {
729
- panel.append('<div class="form-row"><label>dataType</label><select id="opcua-create-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
781
+ if (pendingCreate.kind === "variable" || pendingCreate.kind === "enum-variable") {
782
+ var isEnum = pendingCreate.kind === "enum-variable";
783
+ var typeHtml = isEnum ? buildEnumerationSelect("opcua-create-type", pendingCreate.dataType) : '<select id="opcua-create-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Int64">Int64</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select>';
784
+ panel.append('<div class="form-row"><label>dataType</label>' + typeHtml + '</div>');
730
785
  panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-create-value"></div>');
731
786
  panel.append('<div class="form-row"><label>Access</label><select id="opcua-create-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
732
787
  }
@@ -794,6 +849,37 @@
794
849
  if (normalizeNamespaceId(item.id) === DEFAULT_NAMESPACE_ID) $("#opcua-detail-namespace-entry-id").prop("disabled", true);
795
850
  return;
796
851
  }
852
+ if (nodeClass === "Enumeration") {
853
+ panel.append('<div class="form-row"><label>browseName</label><input type="text" id="opcua-detail-name"></div>');
854
+ panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
855
+ panel.append('<div class="form-row"><label>Description</label><input type="text" id="opcua-detail-description"></div>');
856
+ panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname" placeholder="Leave blank to use browseName"></div>');
857
+ panel.append('<hr style="margin:8px 0; border-color:#e3e3e3;">');
858
+ panel.append('<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#666;margin-bottom:4px;">States</div>');
859
+ var statesDiv = $('<div id="opcua-detail-states"></div>').appendTo(panel);
860
+ (item.enumeration || []).forEach(function (state, idx) {
861
+ var statePath = selectedPath + ".enumeration." + idx;
862
+ var stateBlock = $('<div style="border:1px solid #e3e3e3;border-radius:4px;padding:6px;margin-bottom:4px;"></div>');
863
+ stateBlock.append('<div class="form-row"><label>value</label><input type="number" class="opcua-enum-state-bind" data-state-path="' + statePath + '" data-field="value"></div>');
864
+ stateBlock.append('<div class="form-row"><label>displayName</label><input type="text" class="opcua-enum-state-bind" data-state-path="' + statePath + '" data-field="displayName"></div>');
865
+ stateBlock.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-enum-state-remove" data-state-path="' + statePath + '"><i class="fa fa-trash"></i> Remove</a></div>');
866
+ stateBlock.find('[data-field="value"]').val(state.value !== undefined ? state.value : 0);
867
+ stateBlock.find('[data-field="displayName"]').val(state.displayName || "");
868
+ statesDiv.append(stateBlock);
869
+ });
870
+ panel.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small" id="opcua-enum-add-state"><i class="fa fa-plus"></i> Add state</a></div>');
871
+ panel.append('<div class="form-row"><label style="width:90px;">Actions</label><div><a href="#" id="opcua-detail-edit" class="editor-button editor-button-small"><i class="fa fa-pencil"></i> Edit</a> <a href="#" id="opcua-detail-remove" class="editor-button editor-button-small"><i class="fa fa-trash"></i> Remove</a></div></div>');
872
+
873
+ $("#opcua-detail-name").val(item.name || "");
874
+ $("#opcua-detail-description").val(item.description || "");
875
+ namespaceOptions.forEach(function (option) {
876
+ $("#opcua-detail-namespace").append($("<option></option>").val(option.id).text(getNamespaceLabel(option.id)));
877
+ });
878
+ $("#opcua-detail-namespace").val(String(namespaceId));
879
+ $("#opcua-detail-displayname").val(item.displayName || "");
880
+ return;
881
+ }
882
+
797
883
  panel.append('<div class="form-row"><label>browseName</label><input type="text" id="opcua-detail-name"></div>');
798
884
  panel.append('<div class="form-row"><label>nodeClass</label><input type="text" id="opcua-detail-class" readonly></div>');
799
885
  panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
@@ -805,7 +891,12 @@
805
891
  panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-detail-objectstype", item.objectsType || "") + '</div>');
806
892
  }
807
893
  if (nodeClass === "Variable") {
808
- panel.append('<div class="form-row"><label>dataType</label><select id="opcua-detail-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option><option value="ByteString">ByteString</option></select></div>');
894
+ var enumNames = getDefinedEnumerationNames();
895
+ if (enumNames.indexOf(item.type) !== -1) {
896
+ panel.append('<div class="form-row"><label>dataType</label>' + buildEnumerationSelect("opcua-detail-type", item.type) + '</div>');
897
+ } else {
898
+ panel.append('<div class="form-row"><label>dataType</label><select id="opcua-detail-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Int64">Int64</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option><option value="ByteString">ByteString</option></select></div>');
899
+ }
809
900
  panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-detail-value"></div>');
810
901
  panel.append('<div class="form-row"><label>Access</label><select id="opcua-detail-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
811
902
  }
@@ -817,7 +908,7 @@
817
908
  var argPath = selectedPath + ".inputs." + idx;
818
909
  var argBlock = $('<div style="border:1px solid #e3e3e3;border-radius:4px;padding:6px;margin-bottom:4px;"></div>');
819
910
  argBlock.append('<div class="form-row"><label>name</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="name"></div>');
820
- argBlock.append('<div class="form-row"><label>type</label><select class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
911
+ argBlock.append('<div class="form-row"><label>type</label><select class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Int64">Int64</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
821
912
  argBlock.append('<div class="form-row"><label>displayName</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="displayName"></div>');
822
913
  argBlock.append('<div class="form-row"><label>description</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="description"></div>');
823
914
  argBlock.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-method-arg-remove" data-arg-path="' + argPath + '"><i class="fa fa-trash"></i> Remove</a></div>');
@@ -835,7 +926,7 @@
835
926
  var argPath = selectedPath + ".outputs." + idx;
836
927
  var argBlock = $('<div style="border:1px solid #e3e3e3;border-radius:4px;padding:6px;margin-bottom:4px;"></div>');
837
928
  argBlock.append('<div class="form-row"><label>name</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="name"></div>');
838
- argBlock.append('<div class="form-row"><label>type</label><select class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
929
+ argBlock.append('<div class="form-row"><label>type</label><select class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Int64">Int64</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
839
930
  argBlock.append('<div class="form-row"><label>displayName</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="displayName"></div>');
840
931
  argBlock.append('<div class="form-row"><label>description</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="description"></div>');
841
932
  argBlock.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-method-arg-remove" data-arg-path="' + argPath + '"><i class="fa fa-trash"></i> Remove</a></div>');
@@ -968,7 +1059,11 @@
968
1059
 
969
1060
  function removeAuthGroup(index) {
970
1061
  var groupName = authGroups[index];
971
- var inUse = authUsers.some(function (user) { return user.group === groupName; });
1062
+ var inUse = authUsers.some(function (user) {
1063
+ if (!user.group) return false;
1064
+ var groups = String(user.group).split(",").map(function (g) { return g.trim(); });
1065
+ return groups.indexOf(groupName) !== -1;
1066
+ });
972
1067
  if (inUse) {
973
1068
  RED.notify("Reassign users before removing this group.", "warning");
974
1069
  return;
@@ -1009,16 +1104,35 @@
1009
1104
  return;
1010
1105
  }
1011
1106
 
1107
+ var groupOptions = authGroups.map(function (groupName) {
1108
+ return { value: groupName, label: groupName };
1109
+ });
1110
+
1012
1111
  authUsers.forEach(function (user, index) {
1013
1112
  var card = $('<div class="opcua-auth-card"></div>');
1014
1113
  card.append('<div class="form-row"><label>Username</label><input type="text" class="opcua-auth-user-username" data-index="' + index + '"></div>');
1015
1114
  card.append('<div class="form-row"><label>Password</label><input type="password" class="opcua-auth-user-password" data-index="' + index + '" autocomplete="new-password"></div>');
1016
- card.append('<div class="form-row"><label>Group</label><select class="opcua-auth-user-group" data-index="' + index + '">' + buildGroupOptions(user.group) + '</select></div>');
1115
+ card.append('<div class="form-row"><label>Group</label><input type="text" class="opcua-auth-user-group" id="opcua-auth-user-group-' + index + '" data-index="' + index + '"></div>');
1017
1116
  card.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-auth-user-remove" data-index="' + index + '"><i class="fa fa-trash"></i> Remove</a></div>');
1018
1117
  card.find(".opcua-auth-user-username").val(user.username || "");
1019
1118
  card.find(".opcua-auth-user-password").val(user.password || "");
1020
- card.find(".opcua-auth-user-group").val(user.group || "");
1021
1119
  container.append(card);
1120
+
1121
+ $("#opcua-auth-user-group-" + index).typedInput({
1122
+ types: [
1123
+ {
1124
+ value: "groups",
1125
+ multiple: "true",
1126
+ options: groupOptions
1127
+ }
1128
+ ]
1129
+ });
1130
+ $("#opcua-auth-user-group-" + index).typedInput("value", user.group || "");
1131
+ $("#opcua-auth-user-group-" + index).on("change", function () {
1132
+ var idx = Number($(this).attr("data-index"));
1133
+ authUsers[idx].group = $(this).typedInput("value");
1134
+ syncAuthCredentialFields();
1135
+ });
1022
1136
  });
1023
1137
  }
1024
1138
 
@@ -1087,8 +1201,12 @@
1087
1201
  function addItem(parentPath, kind) {
1088
1202
  var target = getAtPath(editorState, parentPath);
1089
1203
  if (!Array.isArray(target)) return;
1090
- if (kind === "object" || kind === "folder" || kind === "objectTypeDefinition") {
1091
- target.push(normalizeBranch());
1204
+ if (kind === "object" || kind === "folder" || kind === "objectTypeDefinition" || kind === "enumeration") {
1205
+ if (kind === "enumeration") {
1206
+ target.push(normalizeEnumeration({ name: "newEnumeration", enumeration: [{ value: 0, displayName: "State0" }] }));
1207
+ } else {
1208
+ target.push(normalizeBranch());
1209
+ }
1092
1210
  if (kind === "objectTypeDefinition") {
1093
1211
  target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(parentPath + "." + (target.length - 1));
1094
1212
  }
@@ -1140,6 +1258,8 @@
1140
1258
  if (action === "add-object") addNode(selectedPath, "object");
1141
1259
  if (action === "add-variable") addNode(selectedPath, "variable");
1142
1260
  if (action === "add-objecttype") addNode(selectedPath, "objecttype");
1261
+ if (action === "add-enumeration") { addItem("enumerations", "enumeration"); syncStateToJson(true); renderVisualEditor(); }
1262
+ if (action === "add-enum-variable") addNode(selectedPath, "enum-variable");
1143
1263
  if (action === "add-alarm") addNode(selectedPath, "alarm");
1144
1264
  if (action === "add-method") addNode(selectedPath, "method");
1145
1265
  if (action === "add-method") addNode(selectedPath, "method");
@@ -1186,6 +1306,36 @@
1186
1306
  renderDetails();
1187
1307
  });
1188
1308
 
1309
+ $(document).on("input change", ".opcua-enum-state-bind", function () {
1310
+ var el = $(this);
1311
+ var statePath = el.attr("data-state-path");
1312
+ var field = el.attr("data-field");
1313
+ var state = getAtPath(editorState, statePath);
1314
+ if (!state) return;
1315
+ state[field] = field === "value" ? Number(el.val()) : el.val();
1316
+ syncStateToJson(false);
1317
+ });
1318
+
1319
+ $(document).on("click", ".opcua-enum-state-remove", function (e) {
1320
+ e.preventDefault();
1321
+ var statePath = $(this).attr("data-state-path");
1322
+ removeAtPath(editorState, statePath);
1323
+ syncStateToJson(false);
1324
+ renderDetails();
1325
+ });
1326
+
1327
+ $(document).on("click", "#opcua-enum-add-state", function (e) {
1328
+ e.preventDefault();
1329
+ var item = getAtPath(editorState, selectedPath);
1330
+ if (!item) return;
1331
+ if (!Array.isArray(item.enumeration)) item.enumeration = [];
1332
+ var maxValue = -1;
1333
+ item.enumeration.forEach(function (s) { if (s.value > maxValue) maxValue = s.value; });
1334
+ item.enumeration.push(normalizeEnumerationState({ value: maxValue + 1, displayName: "State" + (maxValue + 1) }));
1335
+ syncStateToJson(false);
1336
+ renderDetails();
1337
+ });
1338
+
1189
1339
  $(document).on("input", "#opcua-detail-displayname", function () { updateNode(selectedPath, { displayName: $(this).val() }); });
1190
1340
 
1191
1341
  $(document).on("input", "#opcua-detail-name", function () { updateNode(selectedPath, { name: $(this).val() }); });
@@ -1285,8 +1435,12 @@
1285
1435
  var nextGroup = String(input.val() || "").trim();
1286
1436
  authGroups[index] = nextGroup;
1287
1437
  authUsers.forEach(function (user) {
1288
- if (user.group === previousGroup) {
1289
- user.group = nextGroup;
1438
+ if (user.group) {
1439
+ var groups = String(user.group).split(",").map(function (g) { return g.trim(); });
1440
+ var updated = groups.map(function (g) {
1441
+ return g === previousGroup ? nextGroup : g;
1442
+ });
1443
+ user.group = updated.join(",");
1290
1444
  }
1291
1445
  });
1292
1446
  input.attr("data-previous", nextGroup);
@@ -1310,12 +1464,7 @@
1310
1464
  authUsers[index].passwordHash = "";
1311
1465
  syncAuthCredentialFields();
1312
1466
  });
1313
- $(document).on("change", ".opcua-auth-user-group", function () {
1314
- var index = Number($(this).attr("data-index"));
1315
- authUsers[index].group = $(this).val();
1316
- syncAuthCredentialFields();
1317
-
1318
- });
1467
+ // The change handler is now dynamically bound inside renderAuthUsers, so we do not need a global listener for it here.
1319
1468
  $(document).on("click", ".opcua-auth-user-remove", function (event) {
1320
1469
  event.preventDefault();
1321
1470
  removeAuthUser(Number($(this).attr("data-index")));
@@ -1403,6 +1552,7 @@
1403
1552
  $("#node-input-add-object").off("click").on("click", function (event) { event.preventDefault(); addItem("objects", "object"); syncStateToJson(true); renderVisualEditor(); });
1404
1553
  $("#node-input-add-folder").off("click").on("click", function (event) { event.preventDefault(); addItem("folders", "folder"); syncStateToJson(true); renderVisualEditor(); });
1405
1554
  $("#node-input-add-object-type").off("click").on("click", function (event) { event.preventDefault(); addItem("objectsTypes", "objectTypeDefinition"); syncStateToJson(true); renderVisualEditor(); });
1555
+ $("#node-input-add-enumeration").off("click").on("click", function (event) { event.preventDefault(); addItem("enumerations", "enumeration"); syncStateToJson(true); renderVisualEditor(); });
1406
1556
  $("#node-input-add-namespace").off("click").on("click", function (event) { event.preventDefault(); addItem("nameSpaces", "namespace"); syncStateToJson(true); renderVisualEditor(); });
1407
1557
  $("#node-input-expand-all").off("click").on("click", function (event) {
1408
1558
  event.preventDefault();