@shockers/node-red-plugin-grouped-flows 0.1.26 → 0.1.31

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.
Files changed (2) hide show
  1. package/grouped-flows.html +139 -26
  2. package/package.json +1 -1
@@ -3,7 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  var PLUGIN_ID = "nr-grouped-flows";
6
- var PLUGIN_VERSION = "0.1.26";
6
+ var PLUGIN_VERSION = "0.1.31";
7
7
  var ORIGINAL_UL_ID = "red-ui-workspace-tabs";
8
8
  var CONTAINER_ID = "nr-grouped-flows";
9
9
  var GROUPS_UL_ID = "nr-grouped-flows-groups";
@@ -247,7 +247,25 @@
247
247
  }
248
248
 
249
249
  function makeStableGroupTabId(groupName) {
250
- return "nr-group-tab-" + encodeURIComponent(String(groupName || ""));
250
+ var normalized = String(groupName || "");
251
+ var slug = normalized
252
+ .toLowerCase()
253
+ .replace(/\s+/g, "-")
254
+ .replace(/[^a-z0-9_-]+/g, "-")
255
+ .replace(/-+/g, "-")
256
+ .replace(/^-+|-+$/g, "");
257
+ if (!slug) {
258
+ slug = "group";
259
+ }
260
+
261
+ // Deterministic hash keeps IDs stable and avoids collisions for similar slugs.
262
+ var hash = 2166136261;
263
+ for (var i = 0; i < normalized.length; i += 1) {
264
+ hash ^= normalized.charCodeAt(i);
265
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
266
+ }
267
+ var suffix = (hash >>> 0).toString(36);
268
+ return "nr-group-tab-" + slug + "-" + suffix;
251
269
  }
252
270
 
253
271
  function makeStableTopFlowTabId(workspaceId) {
@@ -407,14 +425,63 @@
407
425
  };
408
426
  }
409
427
 
410
- function clearTabs(tabApi) {
411
- if (!tabApi || typeof tabApi.listTabs !== "function") {
428
+ function getTabId(tab) {
429
+ if (!tab) {
430
+ return "";
431
+ }
432
+ if (typeof tab === "string") {
433
+ return tab;
434
+ }
435
+ if (typeof tab.id === "string" && tab.id) {
436
+ return tab.id;
437
+ }
438
+ if (typeof tab.tabId === "string" && tab.tabId) {
439
+ return tab.tabId;
440
+ }
441
+ if (typeof tab.href === "string" && tab.href.charAt(0) === "#") {
442
+ return tab.href.slice(1);
443
+ }
444
+ return "";
445
+ }
446
+
447
+ function listTabIds(tabApi, ulId) {
448
+ var tabIds = [];
449
+ if (tabApi && typeof tabApi.listTabs === "function") {
450
+ (tabApi.listTabs() || []).forEach(function(tabEntry) {
451
+ var tabId = getTabId(tabEntry);
452
+ if (tabId) {
453
+ tabIds.push(tabId);
454
+ }
455
+ });
456
+ }
457
+
458
+ if (!tabIds.length && ulId) {
459
+ $("#" + ulId + " a.red-ui-tab-label[href^='#']").each(function() {
460
+ var href = $(this).attr("href") || "";
461
+ if (href.charAt(0) === "#") {
462
+ tabIds.push(href.slice(1));
463
+ }
464
+ });
465
+ }
466
+
467
+ var uniqueTabIds = [];
468
+ var seen = {};
469
+ tabIds.forEach(function(tabId) {
470
+ if (!seen[tabId]) {
471
+ seen[tabId] = true;
472
+ uniqueTabIds.push(tabId);
473
+ }
474
+ });
475
+ return uniqueTabIds;
476
+ }
477
+
478
+ function clearTabs(tabApi, ulId) {
479
+ if (!tabApi || typeof tabApi.removeTab !== "function") {
412
480
  return;
413
481
  }
414
-
415
- var existing = tabApi.listTabs() || [];
416
- existing.slice().forEach(function(id) {
417
- tabApi.removeTab(id);
482
+
483
+ listTabIds(tabApi, ulId).forEach(function(tabId) {
484
+ tabApi.removeTab(tabId);
418
485
  });
419
486
  }
420
487
 
@@ -486,6 +553,40 @@
486
553
  }
487
554
  });
488
555
  }
556
+
557
+ function getGroupKeyForTab(tab) {
558
+ if (tab && typeof tab.groupKey === "string" && tab.groupKey) {
559
+ return tab.groupKey;
560
+ }
561
+ var tabId = getTabId(tab);
562
+ if (!tabId) {
563
+ var groupHref = $("#" + GROUPS_UL_ID + " li.red-ui-tab.active a.red-ui-tab-label").first().attr("href") || "";
564
+ if (groupHref.charAt(0) === "#") {
565
+ tabId = groupHref.slice(1);
566
+ }
567
+ }
568
+ if (tabId && state.groupTabGroupMap[tabId]) {
569
+ return state.groupTabGroupMap[tabId];
570
+ }
571
+ return "";
572
+ }
573
+
574
+ function getWorkspaceIdForFlowTab(tab) {
575
+ if (tab && typeof tab.workspaceId === "string" && tab.workspaceId) {
576
+ return tab.workspaceId;
577
+ }
578
+ var tabId = getTabId(tab);
579
+ if (!tabId) {
580
+ var flowHref = $("#" + FLOWS_UL_ID + " li.red-ui-tab.active a.red-ui-tab-label").first().attr("href") || "";
581
+ if (flowHref.charAt(0) === "#") {
582
+ tabId = flowHref.slice(1);
583
+ }
584
+ }
585
+ if (tabId && state.flowTabWorkspaceMap[tabId]) {
586
+ return state.flowTabWorkspaceMap[tabId];
587
+ }
588
+ return "";
589
+ }
489
590
 
490
591
  function applyFlowReorder(newOrder) {
491
592
  if (state.applyingFlowReorder) {
@@ -828,7 +929,7 @@
828
929
 
829
930
  function resetFlowTabsState() {
830
931
  state.suspendFlowChange = true;
831
- clearTabs(state.flowTabs);
932
+ clearTabs(state.flowTabs, FLOWS_UL_ID);
832
933
  state.suspendFlowChange = false;
833
934
  state.flowTabWorkspaceMap = {};
834
935
  state.lastRenderedFlowGroup = null;
@@ -866,8 +967,10 @@
866
967
  if (!workspace || !input.length) {
867
968
  return;
868
969
  }
869
-
870
- var oldValue = normalizeGroupName(workspace[WORKSPACE_GROUP_PROPERTY]);
970
+
971
+ var oldValue = normalizeGroupName(
972
+ workspace[WORKSPACE_GROUP_PROPERTY] || getPersistedGroup(workspaceId)
973
+ );
871
974
  var newValue = normalizeGroupName(input.val());
872
975
  if (newValue === oldValue) {
873
976
  return;
@@ -938,7 +1041,9 @@
938
1041
  }
939
1042
  }
940
1043
 
941
- row.find("#" + DIALOG_GROUP_INPUT_ID).val(normalizeGroupName(workspace[WORKSPACE_GROUP_PROPERTY]));
1044
+ row.find("#" + DIALOG_GROUP_INPUT_ID).val(
1045
+ normalizeGroupName(workspace[WORKSPACE_GROUP_PROPERTY] || getPersistedGroup(workspaceId))
1046
+ );
942
1047
  bindDialogGroupSave(workspaceId, form);
943
1048
  return true;
944
1049
  }
@@ -1053,7 +1158,7 @@
1053
1158
 
1054
1159
  if (groupsChanged) {
1055
1160
  state.suspendGroupChange = true;
1056
- clearTabs(state.groupTabs);
1161
+ clearTabs(state.groupTabs, GROUPS_UL_ID);
1057
1162
  state.groupTabGroupMap = {};
1058
1163
 
1059
1164
  state.model.groups.forEach(function(group) {
@@ -1128,7 +1233,7 @@
1128
1233
 
1129
1234
  if (flowsChanged) {
1130
1235
  state.suspendFlowChange = true;
1131
- clearTabs(state.flowTabs);
1236
+ clearTabs(state.flowTabs, FLOWS_UL_ID);
1132
1237
  state.flowTabWorkspaceMap = {};
1133
1238
 
1134
1239
  selectedGroup.flows.forEach(function(flow) {
@@ -1335,16 +1440,22 @@
1335
1440
  applyGroupReorder(newOrder);
1336
1441
  },
1337
1442
  onchange: function(tab) {
1338
- if (state.suspendGroupChange || !tab) {
1443
+ if (state.suspendGroupChange) {
1444
+ return;
1445
+ }
1446
+ var groupKey = getGroupKeyForTab(tab);
1447
+ if (!groupKey) {
1448
+ debug("group onchange ignored: missing group key", { tab: tab });
1339
1449
  return;
1340
1450
  }
1341
- onGroupChange(tab.groupKey);
1451
+ onGroupChange(groupKey);
1342
1452
  },
1343
1453
  ondblclick: function(tab) {
1344
- if (!tab || !tab.groupKey) {
1454
+ var groupKey = getGroupKeyForTab(tab);
1455
+ if (!groupKey) {
1345
1456
  return;
1346
1457
  }
1347
- var entry = state.model.groupByName[tab.groupKey];
1458
+ var entry = state.model.groupByName[groupKey];
1348
1459
  if (!entry || !entry.standalone || !entry.standaloneWorkspaceId) {
1349
1460
  return;
1350
1461
  }
@@ -1364,30 +1475,32 @@
1364
1475
  applyFlowReorder(newOrder);
1365
1476
  },
1366
1477
  onchange: function(tab) {
1367
- if (state.suspendFlowChange || !tab || !tab.workspaceId) {
1478
+ var workspaceId = getWorkspaceIdForFlowTab(tab);
1479
+ if (state.suspendFlowChange || !workspaceId) {
1368
1480
  return;
1369
1481
  }
1370
- activateCoreWorkspace(tab.workspaceId);
1482
+ activateCoreWorkspace(workspaceId);
1371
1483
  },
1372
1484
  ondblclick: function(tab) {
1373
- if (!tab || !tab.workspaceId) {
1485
+ var workspaceId = getWorkspaceIdForFlowTab(tab);
1486
+ if (!workspaceId) {
1374
1487
  debug("flow ondblclick ignored: missing tab/workspace");
1375
1488
  return;
1376
1489
  }
1377
1490
  debug("flow ondblclick received", {
1378
- workspaceId: tab.workspaceId,
1491
+ workspaceId: workspaceId,
1379
1492
  pending: state.pendingFlowDoubleWorkspaceId
1380
1493
  });
1381
- if (state.pendingFlowDoubleWorkspaceId !== tab.workspaceId) {
1494
+ if (state.pendingFlowDoubleWorkspaceId !== workspaceId) {
1382
1495
  debug("flow ondblclick blocked by guard", {
1383
- workspaceId: tab.workspaceId,
1496
+ workspaceId: workspaceId,
1384
1497
  pending: state.pendingFlowDoubleWorkspaceId
1385
1498
  });
1386
1499
  return;
1387
1500
  }
1388
1501
  state.pendingFlowDoubleWorkspaceId = null;
1389
- debug("flow ondblclick accepted", { workspaceId: tab.workspaceId });
1390
- openWorkspaceEditDialog(tab.workspaceId);
1502
+ debug("flow ondblclick accepted", { workspaceId: workspaceId });
1503
+ openWorkspaceEditDialog(workspaceId);
1391
1504
  }
1392
1505
  });
1393
1506
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shockers/node-red-plugin-grouped-flows",
3
- "version": "0.1.26",
3
+ "version": "0.1.31",
4
4
  "description": "Replaces the built-in Node-RED flows tab bar with a grouped two-tier tab bar to improve navigation of large Node-RED environments.",
5
5
  "author": "Sven Hockers",
6
6
  "license": "Apache-2.0",