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

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 +132 -23
  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.30";
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;
@@ -1053,7 +1154,7 @@
1053
1154
 
1054
1155
  if (groupsChanged) {
1055
1156
  state.suspendGroupChange = true;
1056
- clearTabs(state.groupTabs);
1157
+ clearTabs(state.groupTabs, GROUPS_UL_ID);
1057
1158
  state.groupTabGroupMap = {};
1058
1159
 
1059
1160
  state.model.groups.forEach(function(group) {
@@ -1128,7 +1229,7 @@
1128
1229
 
1129
1230
  if (flowsChanged) {
1130
1231
  state.suspendFlowChange = true;
1131
- clearTabs(state.flowTabs);
1232
+ clearTabs(state.flowTabs, FLOWS_UL_ID);
1132
1233
  state.flowTabWorkspaceMap = {};
1133
1234
 
1134
1235
  selectedGroup.flows.forEach(function(flow) {
@@ -1335,16 +1436,22 @@
1335
1436
  applyGroupReorder(newOrder);
1336
1437
  },
1337
1438
  onchange: function(tab) {
1338
- if (state.suspendGroupChange || !tab) {
1439
+ if (state.suspendGroupChange) {
1440
+ return;
1441
+ }
1442
+ var groupKey = getGroupKeyForTab(tab);
1443
+ if (!groupKey) {
1444
+ debug("group onchange ignored: missing group key", { tab: tab });
1339
1445
  return;
1340
1446
  }
1341
- onGroupChange(tab.groupKey);
1447
+ onGroupChange(groupKey);
1342
1448
  },
1343
1449
  ondblclick: function(tab) {
1344
- if (!tab || !tab.groupKey) {
1450
+ var groupKey = getGroupKeyForTab(tab);
1451
+ if (!groupKey) {
1345
1452
  return;
1346
1453
  }
1347
- var entry = state.model.groupByName[tab.groupKey];
1454
+ var entry = state.model.groupByName[groupKey];
1348
1455
  if (!entry || !entry.standalone || !entry.standaloneWorkspaceId) {
1349
1456
  return;
1350
1457
  }
@@ -1364,30 +1471,32 @@
1364
1471
  applyFlowReorder(newOrder);
1365
1472
  },
1366
1473
  onchange: function(tab) {
1367
- if (state.suspendFlowChange || !tab || !tab.workspaceId) {
1474
+ var workspaceId = getWorkspaceIdForFlowTab(tab);
1475
+ if (state.suspendFlowChange || !workspaceId) {
1368
1476
  return;
1369
1477
  }
1370
- activateCoreWorkspace(tab.workspaceId);
1478
+ activateCoreWorkspace(workspaceId);
1371
1479
  },
1372
1480
  ondblclick: function(tab) {
1373
- if (!tab || !tab.workspaceId) {
1481
+ var workspaceId = getWorkspaceIdForFlowTab(tab);
1482
+ if (!workspaceId) {
1374
1483
  debug("flow ondblclick ignored: missing tab/workspace");
1375
1484
  return;
1376
1485
  }
1377
1486
  debug("flow ondblclick received", {
1378
- workspaceId: tab.workspaceId,
1487
+ workspaceId: workspaceId,
1379
1488
  pending: state.pendingFlowDoubleWorkspaceId
1380
1489
  });
1381
- if (state.pendingFlowDoubleWorkspaceId !== tab.workspaceId) {
1490
+ if (state.pendingFlowDoubleWorkspaceId !== workspaceId) {
1382
1491
  debug("flow ondblclick blocked by guard", {
1383
- workspaceId: tab.workspaceId,
1492
+ workspaceId: workspaceId,
1384
1493
  pending: state.pendingFlowDoubleWorkspaceId
1385
1494
  });
1386
1495
  return;
1387
1496
  }
1388
1497
  state.pendingFlowDoubleWorkspaceId = null;
1389
- debug("flow ondblclick accepted", { workspaceId: tab.workspaceId });
1390
- openWorkspaceEditDialog(tab.workspaceId);
1498
+ debug("flow ondblclick accepted", { workspaceId: workspaceId });
1499
+ openWorkspaceEditDialog(workspaceId);
1391
1500
  }
1392
1501
  });
1393
1502
 
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.30",
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",