@bldgblocks/node-red-contrib-control 0.1.37 → 0.2.0

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.
@@ -8,7 +8,7 @@ module.exports = function(RED) {
8
8
  node.contextPropertyName = config.contextPropertyName || "in1";
9
9
  node.removeLabel = config.removeLabel || false;
10
10
 
11
- utils.setStatusOK(node, `mode: ${node.removeLabel ? "remove" : "set"}, property: ${node.contextPropertyName}`);
11
+ utils.setStatusOK(node, node.removeLabel ? "remove" : `set -> ${node.contextPropertyName}`);
12
12
 
13
13
  node.on("input", function(msg, send, done) {
14
14
  send = send || function() { node.send.apply(node, arguments); };
@@ -23,10 +23,10 @@ module.exports = function(RED) {
23
23
  // Set or remove context property
24
24
  if (node.removeLabel) {
25
25
  delete msg.context;
26
- utils.setStatusChanged(node, `in: ${msg.payload}, out: removed context`);
26
+ utils.setStatusChanged(node, `${msg.payload} -> removed`);
27
27
  } else {
28
28
  msg.context = node.contextPropertyName;
29
- utils.setStatusChanged(node, `in: ${msg.payload}, out: ${node.contextPropertyName}`);
29
+ utils.setStatusChanged(node, `${msg.payload} -> ${node.contextPropertyName}`);
30
30
  }
31
31
 
32
32
  send(msg);
@@ -5,14 +5,19 @@ module.exports = function(RED) {
5
5
  RED.nodes.createNode(this, config);
6
6
  const node = this;
7
7
 
8
+ // Unit multipliers (constant, computed once)
9
+ const delayOnMultiplier = config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1;
10
+ const delayOffMultiplier = config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1;
11
+
8
12
  // Initialize state
9
13
  node.name = config.name;
10
14
  node.state = false;
11
15
  node.desired = false;
12
- node.delayOn = parseFloat(config.delayOn) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
13
- node.delayOff = parseFloat(config.delayOff) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
16
+ node.delayOn = parseFloat(config.delayOn) * delayOnMultiplier;
17
+ node.delayOff = parseFloat(config.delayOff) * delayOffMultiplier;
14
18
 
15
19
  let timeoutId = null;
20
+ let pendingDone = null; // Track deferred done() for in-flight timers
16
21
  node.isBusy = false;
17
22
 
18
23
  node.on("input", async function(msg, send, done) {
@@ -46,21 +51,21 @@ module.exports = function(RED) {
46
51
  utils.requiresEvaluation(config.delayOnType)
47
52
  ? utils.evaluateNodeProperty(config.delayOn, config.delayOnType, node, msg)
48
53
  .then(val => parseFloat(val))
49
- : Promise.resolve(node.delayOn),
54
+ : Promise.resolve(parseFloat(config.delayOn)),
50
55
  );
51
56
 
52
57
  evaluations.push(
53
58
  utils.requiresEvaluation(config.delayOffType)
54
59
  ? utils.evaluateNodeProperty(config.delayOff, config.delayOffType, node, msg)
55
60
  .then(val => parseFloat(val))
56
- : Promise.resolve(node.delayOff),
61
+ : Promise.resolve(parseFloat(config.delayOff)),
57
62
  );
58
63
 
59
64
  const results = await Promise.all(evaluations);
60
65
 
61
- // Update runtime with evaluated values
62
- if (!isNaN(results[0])) node.delayOn = results[0] * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
63
- if (!isNaN(results[1])) node.delayOff = results[1] * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
66
+ // Update runtime with evaluated values (apply unit multiplier to raw config value)
67
+ if (!isNaN(results[0])) node.delayOn = results[0] * delayOnMultiplier;
68
+ if (!isNaN(results[1])) node.delayOff = results[1] * delayOffMultiplier;
64
69
  } catch (err) {
65
70
  node.error(`Error evaluating properties: ${err.message}`);
66
71
  if (done) done();
@@ -98,7 +103,13 @@ module.exports = function(RED) {
98
103
  clearTimeout(timeoutId);
99
104
  timeoutId = null;
100
105
  }
106
+ // Complete any deferred done from a cancelled timer
107
+ if (pendingDone) {
108
+ pendingDone();
109
+ pendingDone = null;
110
+ }
101
111
  node.state = false;
112
+ node.desired = false;
102
113
  utils.setStatusOK(node, "reset");
103
114
  }
104
115
  if (done) done();
@@ -160,44 +171,89 @@ module.exports = function(RED) {
160
171
 
161
172
  if (!node.state && inputValue === true) {
162
173
  if (node.desired) {
174
+ // Already awaiting true, ignore duplicate
163
175
  if (done) done();
164
176
  return;
165
177
  }
166
178
  if (timeoutId) {
167
179
  clearTimeout(timeoutId);
180
+ timeoutId = null;
181
+ }
182
+ // Complete any prior deferred done before starting new timer
183
+ if (pendingDone) {
184
+ pendingDone();
185
+ pendingDone = null;
168
186
  }
169
187
  utils.setStatusUnchanged(node, "awaiting true");
170
188
  node.desired = true;
189
+
190
+ // Clone msg for the timer callback so we don't hold the original
191
+ const delayedMsg = RED.util.cloneMessage(msg);
192
+ // Defer done — this message isn't complete until the timer fires or is cancelled
193
+ pendingDone = done;
194
+
171
195
  timeoutId = setTimeout(() => {
172
196
  node.state = true;
173
- msg.payload = true;
174
- delete msg.context;
197
+ delayedMsg.payload = true;
198
+ delete delayedMsg.context;
175
199
  utils.setStatusChanged(node, "in: true, out: true");
176
- send(msg);
200
+ send(delayedMsg);
177
201
  timeoutId = null;
202
+ if (pendingDone) {
203
+ pendingDone();
204
+ pendingDone = null;
205
+ }
178
206
  }, node.delayOn);
207
+
208
+ // Don't call done() here — it's deferred to the timer callback
209
+ return;
179
210
  } else if (node.state && inputValue === false) {
180
211
  if (node.desired === false) {
212
+ // Already awaiting false, ignore duplicate
181
213
  if (done) done();
182
214
  return;
183
215
  }
184
216
  if (timeoutId) {
185
217
  clearTimeout(timeoutId);
218
+ timeoutId = null;
219
+ }
220
+ // Complete any prior deferred done before starting new timer
221
+ if (pendingDone) {
222
+ pendingDone();
223
+ pendingDone = null;
186
224
  }
187
225
  utils.setStatusUnchanged(node, "awaiting false");
188
226
  node.desired = false;
227
+
228
+ // Clone msg for the timer callback so we don't hold the original
229
+ const delayedMsg = RED.util.cloneMessage(msg);
230
+ // Defer done — this message isn't complete until the timer fires or is cancelled
231
+ pendingDone = done;
232
+
189
233
  timeoutId = setTimeout(() => {
190
234
  node.state = false;
191
- msg.payload = false;
192
- delete msg.context;
235
+ delayedMsg.payload = false;
236
+ delete delayedMsg.context;
193
237
  utils.setStatusChanged(node, "in: false, out: false");
194
- send(msg);
238
+ send(delayedMsg);
195
239
  timeoutId = null;
240
+ if (pendingDone) {
241
+ pendingDone();
242
+ pendingDone = null;
243
+ }
196
244
  }, node.delayOff);
245
+
246
+ // Don't call done() here — it's deferred to the timer callback
247
+ return;
197
248
  } else {
198
249
  if (timeoutId) {
199
250
  clearTimeout(timeoutId);
200
251
  timeoutId = null;
252
+ // Complete deferred done from the cancelled timer's message
253
+ if (pendingDone) {
254
+ pendingDone();
255
+ pendingDone = null;
256
+ }
201
257
  utils.setStatusUnchanged(node, `canceled awaiting ${node.state}`);
202
258
  } else {
203
259
  utils.setStatusUnchanged(node, "no change");
@@ -217,6 +273,11 @@ module.exports = function(RED) {
217
273
  clearTimeout(timeoutId);
218
274
  timeoutId = null;
219
275
  }
276
+ // Complete any deferred done on shutdown
277
+ if (pendingDone) {
278
+ pendingDone();
279
+ pendingDone = null;
280
+ }
220
281
  done();
221
282
  });
222
283
  }
@@ -4,12 +4,22 @@
4
4
  <input type="text" id="node-input-name" placeholder="Name">
5
5
  </div>
6
6
 
7
- <div class="form-row">
8
- <label for="node-input-targetNode"><i class="fa fa-crosshairs"></i> Source</label>
9
- <input type="text" id="node-input-targetNode" style="width: calc(70% - 45px);">
10
- <button id="node-config-find-source" class="editor-button" style="margin-left: 5px; width: 40px;" title="Find Source Node">
11
- <i class="fa fa-search"></i>
12
- </button>
7
+ <div class="form-row" style="margin-bottom: 0px;">
8
+ <label><i class="fa fa-crosshairs"></i> Source</label>
9
+ <input type="hidden" id="node-input-targetNode">
10
+ <span id="node-input-source-toggle" style="float: right; cursor: pointer; padding: 2px 6px; font-size: 14px;" title="Collapse/Expand source list">
11
+ <i class="fa fa-caret-down"></i>
12
+ </span>
13
+ </div>
14
+ <div id="node-input-source-summary" style="display: none; padding: 4px 8px; margin-bottom: 5px; border: 1px solid #ddd; border-radius: 4px; background: transparent; color: #888; font-style: italic; cursor: pointer;" title="Click to expand">No source selected</div>
15
+ <div id="node-input-source-expanded">
16
+ <div style="position: relative; height: 30px; text-align: right;">
17
+ <div style="display: inline-block; vertical-align: middle;"><input type="text" id="node-input-source-filter"></div>
18
+ <button id="node-config-find-source" class="editor-button" style="width: 28px; height: 28px; vertical-align: middle; margin-left: 4px; display: inline-flex; align-items: center; justify-content: center;" title="Reveal Source Node">
19
+ <i class="fa fa-search"></i>
20
+ </button>
21
+ </div>
22
+ <div class="form-row node-input-source-row"></div>
13
23
  </div>
14
24
 
15
25
  <div class="form-row">
@@ -74,6 +84,7 @@
74
84
  paletteLabel: "global get",
75
85
  oneditprepare: function() {
76
86
  const node = this;
87
+ let initComplete = false;
77
88
 
78
89
  let candidateNodes = [];
79
90
  RED.nodes.eachNode(function(n) {
@@ -93,10 +104,137 @@
93
104
 
94
105
  candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
95
106
 
96
- $("#node-input-targetNode").typedInput({
97
- types: [{ value: "target", options: candidateNodes }]
107
+ // Build treeList grouped by flow tab (like link-in node)
108
+ const sourceList = $("<div>").css({width: "100%", height: "100%"}).appendTo(".node-input-source-row")
109
+ .treeList({autoSelect: false});
110
+
111
+ const flowMap = {};
112
+ const flows = [];
113
+ RED.nodes.eachWorkspace(function(ws) {
114
+ flowMap[ws.id] = {
115
+ id: ws.id,
116
+ class: 'red-ui-palette-header',
117
+ label: (ws.label || ws.id) + (node.z === ws.id ? " *" : ""),
118
+ expanded: true,
119
+ children: []
120
+ };
121
+ flows.push(flowMap[ws.id]);
122
+ });
123
+
124
+ let candidateNodesCount = 0;
125
+ candidateNodes.forEach(function(opt) {
126
+ if (flowMap[RED.nodes.node(opt.value)?.z]) {
127
+ const targetZ = RED.nodes.node(opt.value).z;
128
+ flowMap[targetZ].children.push({
129
+ id: opt.value,
130
+ node: RED.nodes.node(opt.value),
131
+ label: opt.label,
132
+ radio: true,
133
+ radioGroup: "source-select",
134
+ selected: opt.value === node.targetNode
135
+ });
136
+ candidateNodesCount++;
137
+ }
138
+ });
139
+
140
+ const flowsFiltered = flows.filter(function(f) { return f.children.length > 0; });
141
+ sourceList.treeList('data', flowsFiltered);
142
+
143
+ // Show current node's flow tab
144
+ setTimeout(function() { sourceList.treeList('show', node.z); }, 100);
145
+
146
+ // searchBox filter (same pattern as link-in)
147
+ const search = $("#node-input-source-filter").searchBox({
148
+ style: "compact",
149
+ delay: 300,
150
+ change: function() {
151
+ const val = $(this).val().trim().toLowerCase();
152
+ if (val === "") {
153
+ sourceList.treeList("filter", null);
154
+ search.searchBox("count", "");
155
+ } else {
156
+ const count = sourceList.treeList("filter", function(item) {
157
+ return item.label && item.label.toLowerCase().indexOf(val) > -1;
158
+ });
159
+ search.searchBox("count", count + " / " + candidateNodesCount);
160
+ }
161
+ }
98
162
  });
99
163
 
164
+ // Helper to get the currently selected source from the treeList
165
+ function getSelectedSourceId() {
166
+ const items = sourceList.treeList('data');
167
+ for (let f = 0; f < items.length; f++) {
168
+ const children = items[f].children || [];
169
+ for (let c = 0; c < children.length; c++) {
170
+ if (children[c].selected) return children[c].id;
171
+ }
172
+ }
173
+ return "";
174
+ }
175
+
176
+ // --- Collapse / Expand toggle ---
177
+ const toggleBtn = $("#node-input-source-toggle");
178
+ const expandedSection = $("#node-input-source-expanded");
179
+ const summaryBar = $("#node-input-source-summary");
180
+ let sourceExpanded = true;
181
+
182
+ function getSelectedLabel() {
183
+ const id = getSelectedSourceId();
184
+ if (!id) return "No source selected";
185
+ const opt = candidateNodes.find(o => o.value === id);
186
+ return opt ? opt.label : "Unknown";
187
+ }
188
+
189
+ function resizeSourceList() {
190
+ if (!sourceExpanded) return;
191
+ var dialogForm = $("#dialog-form");
192
+ var height = dialogForm.height();
193
+ dialogForm.children().each(function() {
194
+ var $el = $(this);
195
+ if ($el.attr("id") === "node-input-source-expanded") {
196
+ // Subtract the search/button bar inside expanded, but not the source-row itself
197
+ $el.children().each(function() {
198
+ if (!$(this).hasClass("node-input-source-row")) {
199
+ height -= $(this).outerHeight(true);
200
+ }
201
+ });
202
+ } else {
203
+ height -= $el.outerHeight(true);
204
+ }
205
+ });
206
+ $(".node-input-source-row").css("height", Math.max(200, height) + "px");
207
+ }
208
+
209
+ function setSourceExpanded(expanded) {
210
+ sourceExpanded = expanded;
211
+ if (expanded) {
212
+ summaryBar.hide();
213
+ expandedSection.show();
214
+ toggleBtn.find("i").removeClass("fa-caret-right").addClass("fa-caret-down");
215
+ setTimeout(resizeSourceList, 50);
216
+ } else {
217
+ expandedSection.hide();
218
+ summaryBar.text(getSelectedLabel()).show();
219
+ toggleBtn.find("i").removeClass("fa-caret-down").addClass("fa-caret-right");
220
+ }
221
+ }
222
+
223
+ toggleBtn.on("click", function() { setSourceExpanded(!sourceExpanded); });
224
+ summaryBar.on("click", function() { setSourceExpanded(true); });
225
+
226
+ // Update summary text and hidden input when selection changes
227
+ sourceList.on('treelistselect', function() {
228
+ if (!initComplete) return;
229
+ $("#node-input-targetNode").val(getSelectedSourceId());
230
+ if (!sourceExpanded) summaryBar.text(getSelectedLabel());
231
+ });
232
+
233
+ // Start collapsed if a source is already selected
234
+ if (node.targetNode) {
235
+ setSourceExpanded(false);
236
+ }
237
+
100
238
  $("#node-input-outputProperty").typedInput({
101
239
  default: "msg",
102
240
  types: ["msg", "flow",
@@ -110,10 +248,12 @@
110
248
  }).typedInput("type", node.outputPropertyType || "msg").typedInput("value", node.outputProperty);
111
249
 
112
250
  function updateOutputValue() {
251
+ if (!initComplete) return;
113
252
  const currentType = $("#node-input-outputProperty").typedInput("type");
253
+ const currentValue = $("#node-input-outputProperty").typedInput("value");
114
254
 
115
- if (currentType === "dropdown" && node.outputProperty === "sourceToFlow") {
116
- const selectedSourceId = $("#node-input-targetNode").val();
255
+ if (currentType === "dropdown" && currentValue === "sourceToFlow") {
256
+ const selectedSourceId = getSelectedSourceId();
117
257
  const selectedOption = candidateNodes.find(opt => opt.value === selectedSourceId);
118
258
 
119
259
  if (selectedOption && selectedOption.path) {
@@ -122,14 +262,35 @@
122
262
  }
123
263
  }
124
264
 
125
- $("#node-input-targetNode").on("change", updateOutputValue);
265
+ sourceList.on('treelistselect', updateOutputValue);
126
266
  $("#node-input-outputProperty").on("change", updateOutputValue);
127
267
 
128
268
  $("#node-config-find-source").on("click", function() {
129
- const selectedId = $("#node-input-targetNode").val();
269
+ const selectedId = getSelectedSourceId();
130
270
  if (selectedId) { RED.view.reveal(selectedId); }
131
271
  else { RED.notify("Please select a source node first.", "warning"); }
132
272
  });
273
+
274
+ // Mark init complete - all event handlers now active
275
+ setTimeout(function() { initComplete = true; }, 200);
276
+ },
277
+ oneditresize: function(size) {
278
+ if ($("#node-input-source-expanded").is(":hidden")) return;
279
+ var dialogForm = $("#dialog-form");
280
+ var height = dialogForm.height();
281
+ dialogForm.children().each(function() {
282
+ var $el = $(this);
283
+ if ($el.attr("id") === "node-input-source-expanded") {
284
+ $el.children().each(function() {
285
+ if (!$(this).hasClass("node-input-source-row")) {
286
+ height -= $(this).outerHeight(true);
287
+ }
288
+ });
289
+ } else {
290
+ height -= $el.outerHeight(true);
291
+ }
292
+ });
293
+ $(".node-input-source-row").css("height", Math.max(200, height) + "px");
133
294
  }
134
295
  });
135
296
  </script>
@@ -10,10 +10,11 @@ module.exports = function(RED) {
10
10
  node.detail = config.detail;
11
11
 
12
12
  let setterNode = null;
13
- let retryAction = null;
14
- let healthCheckAction = null;
13
+ let retryTimeout = null;
14
+ let healthCheckTimeout = null;
15
15
  let updateListener = null;
16
16
  let retryCount = 0;
17
+ let closed = false;
17
18
  const retryDelays = [0, 100, 500, 1000, 2000, 4000, 8000, 16000];
18
19
  const maxRetries = retryDelays.length - 1;
19
20
 
@@ -85,9 +86,9 @@ module.exports = function(RED) {
85
86
 
86
87
  RED.events.on("bldgblocks:global:value-changed", updateListener);
87
88
 
88
- if (retryAction) {
89
- clearInterval(retryAction);
90
- retryAction = null;
89
+ if (retryTimeout) {
90
+ clearTimeout(retryTimeout);
91
+ retryTimeout = null;
91
92
  }
92
93
 
93
94
  utils.setStatusOK(node, "Connected");
@@ -98,6 +99,11 @@ module.exports = function(RED) {
98
99
 
99
100
  function startHealthCheck() {
100
101
  const check = () => {
102
+ if (closed) return;
103
+ if (!updateListener) {
104
+ healthCheckTimeout = setTimeout(check, 30000);
105
+ return;
106
+ }
101
107
  const listeners = RED.events.listeners("bldgblocks:global:value-changed");
102
108
  const hasOurListener = listeners.includes(updateListener);
103
109
  if (!hasOurListener) {
@@ -106,13 +112,14 @@ module.exports = function(RED) {
106
112
  utils.setStatusOK(node, "Reconnected");
107
113
  }
108
114
  }
109
- setTimeout(check, 30000);
115
+ healthCheckTimeout = setTimeout(check, 30000);
110
116
  };
111
- setTimeout(check, 30000);
117
+ healthCheckTimeout = setTimeout(check, 30000);
112
118
  }
113
119
 
114
120
  function subscribeWithRetry() {
115
- retryAction = () => {
121
+ function attempt() {
122
+ if (closed) return;
116
123
  if (retryCount >= maxRetries) {
117
124
  utils.sendError(node, null, null, "Connection failed");
118
125
  return;
@@ -122,9 +129,9 @@ module.exports = function(RED) {
122
129
  return;
123
130
  }
124
131
  retryCount++;
125
- setTimeout(retryAction, retryDelays[Math.min(retryCount, maxRetries - 1)]);
126
- };
127
- setTimeout(retryAction, retryDelays[0]);
132
+ retryTimeout = setTimeout(attempt, retryDelays[Math.min(retryCount, maxRetries - 1)]);
133
+ }
134
+ retryTimeout = setTimeout(attempt, retryDelays[0]);
128
135
  }
129
136
 
130
137
  // --- INPUT HANDLER ---
@@ -160,10 +167,18 @@ module.exports = function(RED) {
160
167
  }
161
168
 
162
169
  node.on('close', function(removed, done) {
163
- if (healthCheckAction) clearInterval(healthCheckAction);
164
- if (retryAction) clearInterval(retryAction);
165
- if (removed && updateListener) {
170
+ closed = true;
171
+ if (healthCheckTimeout) {
172
+ clearTimeout(healthCheckTimeout);
173
+ healthCheckTimeout = null;
174
+ }
175
+ if (retryTimeout) {
176
+ clearTimeout(retryTimeout);
177
+ retryTimeout = null;
178
+ }
179
+ if (updateListener) {
166
180
  RED.events.removeListener("bldgblocks:global:value-changed", updateListener);
181
+ updateListener = null;
167
182
  }
168
183
  done();
169
184
  });
@@ -25,6 +25,15 @@
25
25
  <input type="hidden" id="node-input-defaultValueType">
26
26
  </div>
27
27
 
28
+ <hr>
29
+
30
+ <div class="form-row">
31
+ <label>&nbsp;</label>
32
+ <button type="button" id="node-btn-clear-priorities" class="editor-button" style="width: calc(70% - 3px);" title="Clear all 16 priority slots on the live node (does not affect the default value)">
33
+ <i class="fa fa-eraser"></i> Clear All Priorities
34
+ </button>
35
+ </div>
36
+
28
37
  <div class="form-tips">
29
38
  <b>Note:</b> This node writes to the selected <b>Priority</b>, manually, by msg or flow. The actual Global Variable value will be the highest active priority.
30
39
  </div>
@@ -97,6 +106,30 @@
97
106
  }, "msg", "flow"],
98
107
  typeField: "#node-input-writePriorityType"
99
108
  }).typedInput("type", node.writePriorityType).typedInput("value", node.writePriority);
109
+
110
+ // Clear Priorities button — calls the admin endpoint on the live runtime node
111
+ $("#node-btn-clear-priorities").on("click", function() {
112
+ const btn = $(this);
113
+ if (!node.id) {
114
+ RED.notify("Node must be deployed before clearing priorities.", "warning");
115
+ return;
116
+ }
117
+ btn.prop("disabled", true).find("i").removeClass("fa-eraser").addClass("fa-spinner fa-spin");
118
+ $.ajax({
119
+ url: "global-setter/" + node.id + "/clear-priorities",
120
+ type: "POST",
121
+ success: function(data) {
122
+ RED.notify("Priorities cleared — active: " + (data.activePriority || "default") + ":" + data.value, "success");
123
+ },
124
+ error: function(jqXHR) {
125
+ const errMsg = jqXHR.responseJSON ? jqXHR.responseJSON.error : "Unknown error";
126
+ RED.notify("Failed to clear priorities: " + errMsg, "error");
127
+ },
128
+ complete: function() {
129
+ btn.prop("disabled", false).find("i").removeClass("fa-spinner fa-spin").addClass("fa-eraser");
130
+ }
131
+ });
132
+ });
100
133
  }
101
134
  });
102
135
  </script>
@@ -108,6 +141,8 @@ Manage a global variable in a repeatable way.
108
141
  ### Inputs
109
142
  : payload (any) : Input payload is passed through unchanged.
110
143
  : property (string) : The input property where the value is taken from.
144
+ : priority (number|string) : _Optional_. Overrides the configured Priority at runtime. Accepts `1`–`16` or `"default"`. For example, `msg.priority = 8` writes to priority slot 8.
145
+ : context (string) : _Optional_. Tagged-input priority routing (matches priority-block conventions). Accepts `"priority1"`–`"priority16"`, `"default"`, or `"reload"`. When both `msg.priority` and `msg.context` are present, `msg.priority` takes precedence.
111
146
  : units (string) : The units associated with the value, if any. Also supports nested units at `msg.<inputProperty>.units`.
112
147
 
113
148
  ### Outputs
@@ -120,6 +155,8 @@ This node allows you to set a global variable in one place, and retrieve it else
120
155
 
121
156
  When this node is deleted or the flow is redeployed, it will automatically remove (prune) the variable from the selected Context Store.
122
157
 
158
+ **Clear All Priorities** button (in editor): Resets all 16 priority slots to `null` on the live running node. The default value is preserved. The active value recalculates to the highest remaining priority (or falls back to default). The node must be deployed first.
159
+
123
160
  ### Status
124
161
  - Green (dot): Configuration update
125
162
  - Blue (dot): State changed