node-red-contrib-join-wait 0.6.2 → 0.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/join-wait.html +172 -191
  3. package/package.json +3 -4
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to this project are documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.3] - 2026-05-09
8
+
9
+ ### Fixed
10
+
11
+ - **Open example flows button** is finally working. The 0.6.2 fix
12
+ invoked the import action via `RED.tray.close(cb)`'s callback, but
13
+ that callback fires before the `editor:close` event — so
14
+ clipboard.js's `disabled` guard was still set when the action ran,
15
+ and the dialog never appeared. Now listens for `editor:close`
16
+ explicitly and defers the action one tick past the synchronous
17
+ listener chain that clears the flag. Adds a guard against fast
18
+ double-clicks and a feature-detect fallback to
19
+ `core:show-import-dialog` for Node-RED < 3.1.
20
+
21
+ ### Changed
22
+
23
+ - **`engines.node` lowered to `>=18.5`** to match the Node-RED 4.x
24
+ minimum (per the contrib-node packaging spec). CI matrix gains
25
+ Node 18 so the claim is verified, not asserted.
26
+ - **Editor template updated to `const`/`let` and arrow functions**.
27
+ Pure stylistic refactor of `join-wait.html`'s IIFE; no behaviour
28
+ change. `function` is preserved where Node-RED binds `this`
29
+ (validators, `oneditprepare`/`save`/`resize`, jQuery `.each`/`.map`
30
+ callbacks).
31
+ - Dropped the `node-red.examples` field from `package.json` — not
32
+ part of the packaging spec; examples auto-discover from
33
+ `examples/`.
34
+
7
35
  ## [0.6.2] - 2026-05-09
8
36
 
9
37
  ### Fixed
package/join-wait.html CHANGED
@@ -139,11 +139,8 @@
139
139
 
140
140
  <script type="text/javascript">
141
141
  (function () {
142
- // Form-element selectors gathered in one place. Keeps the rest
143
- // of the IIFE free of stringly-typed `#node-input-*` lookups
144
- // and makes the editor surface auditable at a glance — every
145
- // hook into the dialog is listed here.
146
- var SEL = {
142
+ // Every DOM hook the editor uses, in one place.
143
+ const SEL = {
147
144
  pathField: '#node-input-pathTopic',
148
145
  pathFieldType: '#node-input-pathTopicType',
149
146
  correlator: '#node-input-correlationTopic',
@@ -163,55 +160,44 @@
163
160
  };
164
161
  // editableList wrapper IDs (no leading '#'); consumers either
165
162
  // pass them to readEditableList or interpolate `#` themselves.
166
- var LIST_ID = {
163
+ const LIST_ID = {
167
164
  paths: 'node-input-paths-container',
168
165
  expire: 'node-input-pathsToExpire-container',
169
166
  };
170
- function listInputs(id) {
171
- return $('#' + id + ' input[type=text]');
172
- }
173
- // Read the typed-input pair behind the Path field — name + type.
174
- // Used wherever both halves are needed together (currently the
175
- // output preview); centralising it avoids drift between the two
176
- // selectors.
177
- function readPathField() {
178
- return {
179
- name: $(SEL.pathField).val() || 'topic',
180
- type: $(SEL.pathFieldType).val() || 'msg',
181
- };
182
- }
167
+ const listInputs = (id) => $('#' + id + ' input[type=text]');
168
+ // Path-field name + typedInput type, read together to avoid drift.
169
+ const readPathField = () => ({
170
+ name: $(SEL.pathField).val() || 'topic',
171
+ type: $(SEL.pathFieldType).val() || 'msg',
172
+ });
183
173
 
184
- function arrayValidator(allowEmpty, requireUnique) {
185
- return function (v) {
186
- // Accept legacy JSON-string format too, for back-compat with old flows.
187
- var arr = v;
188
- if (typeof v === 'string') {
189
- if (v === '') return allowEmpty;
190
- try {
191
- arr = JSON.parse(v);
192
- } catch (err) {
193
- return false;
194
- }
195
- }
196
- if (!Array.isArray(arr)) return false;
197
- if (arr.length === 0) return allowEmpty;
198
- if (
199
- !arr.every(function (s) {
200
- return typeof s === 'string' && s.length > 0;
201
- })
202
- )
174
+ const arrayValidator = (allowEmpty, requireUnique) => (v) => {
175
+ // Accept legacy JSON-string format too, for back-compat with old flows.
176
+ let arr = v;
177
+ if (typeof v === 'string') {
178
+ if (v === '') return allowEmpty;
179
+ try {
180
+ arr = JSON.parse(v);
181
+ } catch (err) {
203
182
  return false;
204
- if (requireUnique) {
205
- var seen = Object.create(null);
206
- for (var i = 0; i < arr.length; i++) {
207
- if (seen[arr[i]]) return false;
208
- seen[arr[i]] = true;
209
- }
210
183
  }
211
- return true;
212
- };
213
- }
184
+ }
185
+ if (!Array.isArray(arr)) return false;
186
+ if (arr.length === 0) return allowEmpty;
187
+ if (!arr.every((s) => typeof s === 'string' && s.length > 0)) return false;
188
+ if (requireUnique) {
189
+ const seen = Object.create(null);
190
+ for (let i = 0; i < arr.length; i++) {
191
+ if (seen[arr[i]]) return false;
192
+ seen[arr[i]] = true;
193
+ }
194
+ }
195
+ return true;
196
+ };
214
197
 
198
+ // Note: Node-RED calls validate functions with `this` bound to the
199
+ // node config, so this stays a function expression (arrow would
200
+ // not bind `this`).
215
201
  function correlationValidator(v) {
216
202
  // Empty / undefined / non-jsonata: nothing to validate here.
217
203
  if (this.correlationTopicType !== 'jsonata' || !v) return true;
@@ -225,27 +211,27 @@
225
211
  }
226
212
  }
227
213
 
228
- function isValidRegex(s) {
214
+ const isValidRegex = (s) => {
229
215
  try {
230
216
  new RegExp(s);
231
217
  return true;
232
218
  } catch (err) {
233
219
  return false;
234
220
  }
235
- }
221
+ };
236
222
 
237
- function setRowValid(input, ok, message) {
223
+ const setRowValid = (input, ok, message) => {
238
224
  input.css('border-color', ok ? '' : '#d9534f');
239
225
  input.attr('title', ok ? '' : message || '');
240
- }
226
+ };
241
227
 
242
228
  // Single-row validation: empty / invalid regex. List-level
243
229
  // checks (uniqueness, cross-list overlap) live in validateAllRows
244
230
  // because they need both lists' current values in scope.
245
- function validateRow(input, ownValues, otherValues, opts) {
246
- var requireUnique = opts && opts.requireUnique;
247
- var otherListLabel = (opts && opts.otherListLabel) || '';
248
- var value = String(input.val() || '');
231
+ const validateRow = (input, ownValues, otherValues, opts) => {
232
+ const requireUnique = opts && opts.requireUnique;
233
+ const otherListLabel = (opts && opts.otherListLabel) || '';
234
+ const value = String(input.val() || '');
249
235
  if (value === '') {
250
236
  setRowValid(input, false, 'path name cannot be empty');
251
237
  return false;
@@ -254,13 +240,7 @@
254
240
  setRowValid(input, false, 'invalid regular expression');
255
241
  return false;
256
242
  }
257
- if (
258
- requireUnique &&
259
- ownValues &&
260
- ownValues.filter(function (x) {
261
- return x === value;
262
- }).length > 1
263
- ) {
243
+ if (requireUnique && ownValues && ownValues.filter((x) => x === value).length > 1) {
264
244
  setRowValid(input, false, 'duplicate entry — must be unique');
265
245
  return false;
266
246
  }
@@ -274,21 +254,21 @@
274
254
  }
275
255
  setRowValid(input, true);
276
256
  return true;
277
- }
257
+ };
278
258
 
279
- // Re-validate every row in both editableLists. Called whenever
280
- // any row changes, the regex toggle flips, or rows are added /
281
- // removed so duplicate and cross-list overlap warnings are
282
- // always in sync with the current state.
283
- function validateAllRows() {
284
- var $paths = listInputs(LIST_ID.paths);
285
- var $expire = listInputs(LIST_ID.expire);
286
- var pathValues = $paths
259
+ // Re-validate every row across both lists so duplicate and
260
+ // cross-list overlap warnings stay in sync.
261
+ const validateAllRows = () => {
262
+ const $paths = listInputs(LIST_ID.paths);
263
+ const $expire = listInputs(LIST_ID.expire);
264
+ // jQuery .map / .each callbacks rely on `this`, so they stay
265
+ // as function expressions.
266
+ const pathValues = $paths
287
267
  .map(function () {
288
268
  return String($(this).val() || '');
289
269
  })
290
270
  .get();
291
- var expireValues = $expire
271
+ const expireValues = $expire
292
272
  .map(function () {
293
273
  return String($(this).val() || '');
294
274
  })
@@ -305,62 +285,52 @@
305
285
  otherListLabel: 'Wait paths',
306
286
  });
307
287
  });
308
- }
288
+ };
309
289
 
310
- function initEditableList(containerId, items, opts) {
311
- var $container = $('#' + containerId);
312
- var placeholder = (opts && opts.placeholder) || 'e.g. sensor_1';
313
- // A numeric height makes editableList scroll its items
314
- // internally past the limit without it (height:'auto'), a
315
- // long list grows the wrapper and visually overlaps the form
316
- // rows that follow. The actual height is recomputed in
317
- // oneditresize to fill the available tray space.
290
+ const initEditableList = (containerId, items, opts) => {
291
+ const $container = $('#' + containerId);
292
+ const placeholder = (opts && opts.placeholder) || 'e.g. sensor_1';
293
+ // Numeric height list scrolls internally past the limit.
294
+ // Recomputed in oneditresize to fill the tray.
318
295
  $container.editableList({
319
- addItem: function (row, index, data) {
320
- var value = (data && data.value) || '';
296
+ addItem: (row, index, data) => {
297
+ const value = (data && data.value) || '';
321
298
  row.css({ overflow: 'hidden', whiteSpace: 'nowrap' });
322
- var input = $('<input/>', {
299
+ const input = $('<input/>', {
323
300
  type: 'text',
324
301
  placeholder: placeholder,
325
302
  style: 'width:100%',
326
303
  }).val(value);
327
- input.on('input change blur', function () {
304
+ input.on('input change blur', () => {
328
305
  validateAllRows();
329
306
  if (opts && opts.onChange) opts.onChange();
330
307
  });
331
308
  // Pressing Enter in a row adds the next one.
332
- input.on('keydown', function (e) {
309
+ input.on('keydown', (e) => {
333
310
  if (e.key === 'Enter' || e.keyCode === 13) {
334
311
  e.preventDefault();
335
312
  $container.editableList('addItem', { value: '' });
336
313
  // Focus the newly-added row.
337
- setTimeout(function () {
314
+ setTimeout(() => {
338
315
  $container.editableList('items').last().find('input').trigger('focus');
339
316
  }, 0);
340
317
  }
341
318
  });
342
- // Bulk-paste: paste newline-separated text into any
343
- // row to fill the current row + add a new row per
344
- // remaining line. Whitespace-only and empty lines
345
- // are dropped. Single-line pastes fall through to
346
- // the browser's default paste handling.
347
- input.on('paste', function (e) {
348
- var clip = (e.originalEvent || e).clipboardData;
319
+ // Bulk-paste newline-separated text into multiple
320
+ // rows. Single-line pastes fall through to default.
321
+ input.on('paste', (e) => {
322
+ const clip = (e.originalEvent || e).clipboardData;
349
323
  if (!clip) return;
350
- var text = clip.getData('text');
324
+ const text = clip.getData('text');
351
325
  if (!text || !/[\r\n]/.test(text)) return;
352
- var lines = text
326
+ const lines = text
353
327
  .split(/\r?\n/)
354
- .map(function (s) {
355
- return s.trim();
356
- })
357
- .filter(function (s) {
358
- return s.length > 0;
359
- });
328
+ .map((s) => s.trim())
329
+ .filter((s) => s.length > 0);
360
330
  if (lines.length === 0) return;
361
331
  e.preventDefault();
362
332
  input.val(lines[0]);
363
- for (var i = 1; i < lines.length; i++) {
333
+ for (let i = 1; i < lines.length; i++) {
364
334
  $container.editableList('addItem', { value: lines[i] });
365
335
  }
366
336
  validateAllRows();
@@ -370,7 +340,7 @@
370
340
  validateAllRows();
371
341
  if (opts && opts.onChange) opts.onChange();
372
342
  },
373
- removeItem: function () {
343
+ removeItem: () => {
374
344
  validateAllRows();
375
345
  if (opts && opts.onChange) opts.onChange();
376
346
  },
@@ -378,95 +348,90 @@
378
348
  sortable: true,
379
349
  height: (opts && opts.height) || 200,
380
350
  });
381
- (items || []).forEach(function (v) {
351
+ (items || []).forEach((v) => {
382
352
  $container.editableList('addItem', { value: v });
383
353
  });
384
- }
354
+ };
385
355
 
386
356
  // Re-validate every editableList row when the regex toggle flips.
387
- function rebindRegexValidation() {
357
+ const rebindRegexValidation = () => {
388
358
  $(SEL.useRegex).on('change', validateAllRows);
389
- }
359
+ };
390
360
 
391
- function readEditableList(containerId) {
392
- var values = [];
361
+ const readEditableList = (containerId) => {
362
+ const values = [];
393
363
  $('#' + containerId)
394
364
  .editableList('items')
395
365
  .each(function () {
396
- var v = $(this).find('input').val();
366
+ // jQuery `each` binds `this` to the row element, so
367
+ // this callback stays a function expression.
368
+ const v = $(this).find('input').val();
397
369
  if (v != null && v !== '') values.push(v);
398
370
  });
399
371
  return values;
400
- }
372
+ };
401
373
 
402
374
  // Coerce legacy string-array values into a plain array for editing.
403
- function toArray(v) {
375
+ const toArray = (v) => {
404
376
  if (Array.isArray(v)) return v;
405
377
  if (typeof v === 'string' && v !== '') {
406
378
  try {
407
- var parsed = JSON.parse(v);
379
+ const parsed = JSON.parse(v);
408
380
  return Array.isArray(parsed) ? parsed : [];
409
381
  } catch (err) {
410
382
  return [];
411
383
  }
412
384
  }
413
385
  return [];
414
- }
386
+ };
415
387
 
416
- // Render a tiny preview of what the path-field property looks
417
- // like on the success output, derived from the current Wait paths
418
- // list. Prefix matches the typed-input type so flow/global don't
419
- // mis-render as msg. Repeated entries surface as `(×n)` so the
420
- // n-of-the-same semantic is visible.
421
- function updateOutputPreview() {
422
- var paths = readEditableList(LIST_ID.paths);
423
- var counts = Object.create(null);
424
- var order = [];
425
- paths.forEach(function (p) {
388
+ // Preview of the merged success output. Repeated entries
389
+ // surface as `(×n)` so the n-of-the-same semantic is visible.
390
+ const updateOutputPreview = () => {
391
+ const paths = readEditableList(LIST_ID.paths);
392
+ const counts = Object.create(null);
393
+ const order = [];
394
+ paths.forEach((p) => {
426
395
  if (counts[p] === undefined) order.push(p);
427
396
  counts[p] = (counts[p] || 0) + 1;
428
397
  });
429
- var field = readPathField();
430
- var $tip = $(SEL.outputPreview);
398
+ const field = readPathField();
399
+ const $tip = $(SEL.outputPreview);
431
400
  if (order.length === 0) {
432
401
  $tip.text('');
433
402
  return;
434
403
  }
435
- var entries = order.slice(0, 4).map(function (p) {
436
- return counts[p] > 1 ? p + ' (×' + counts[p] + '): …' : p + ': …';
437
- });
404
+ const entries = order.slice(0, 4).map((p) => (counts[p] > 1 ? p + ' (×' + counts[p] + '): …' : p + ': …'));
438
405
  if (order.length > 4) entries.push('…');
439
406
  $tip.text('→ ' + field.type + '.' + field.name + ' = { ' + entries.join(', ') + ' }');
440
- }
407
+ };
441
408
 
442
409
  // Warn when the resolved timeout is impractically short (the README
443
410
  // recommends padding ~5–10 ms for evaluation overhead).
444
- function updateTimeoutHint() {
445
- var t = Number($(SEL.timeout).val()) || 0;
446
- var u = Number($(SEL.timeoutUnits).val()) || 1;
447
- var ms = t * u;
448
- var $hint = $(SEL.timeoutHint);
411
+ const updateTimeoutHint = () => {
412
+ const t = Number($(SEL.timeout).val()) || 0;
413
+ const u = Number($(SEL.timeoutUnits).val()) || 1;
414
+ const ms = t * u;
415
+ const $hint = $(SEL.timeoutHint);
449
416
  if (ms > 0 && ms < 50) {
450
417
  $hint.find('span').text('Very short — pad ~10 ms to leave room for evaluation overhead.');
451
418
  $hint.show();
452
419
  } else {
453
420
  $hint.hide();
454
421
  }
455
- }
422
+ };
456
423
 
457
- // Populate the Persist store <select> from the runtime via our
458
- // /join-wait/stores admin route. apiRootUrl prefixes the call so
459
- // it resolves correctly under custom httpAdminRoot / reverse
460
- // proxies; falls back to the bare relative URL on older
461
- // Node-RED versions where the setting isn't exposed.
462
- function populatePersistStores(currentValue) {
463
- var $sel = $(SEL.persistStore);
464
- var base = (RED.settings && RED.settings.apiRootUrl) || '';
424
+ // Populate the Persist store <select> from /join-wait/stores.
425
+ // apiRootUrl prefixes the call so it resolves under custom
426
+ // httpAdminRoot / reverse proxies.
427
+ const populatePersistStores = (currentValue) => {
428
+ const $sel = $(SEL.persistStore);
429
+ const base = (RED.settings && RED.settings.apiRootUrl) || '';
465
430
  $.getJSON(base + 'join-wait/stores')
466
- .done(function (stores) {
467
- (Array.isArray(stores) ? stores : []).forEach(function (s) {
431
+ .done((stores) => {
432
+ (Array.isArray(stores) ? stores : []).forEach((s) => {
468
433
  if (s.name === 'default') return; // already represented as the blank option
469
- var label = s.module ? s.name + ' (' + s.module + ')' : s.name;
434
+ const label = s.module ? s.name + ' (' + s.module + ')' : s.name;
470
435
  $('<option/>').val(s.name).text(label).appendTo($sel);
471
436
  });
472
437
  // If the saved value isn't in the list, append it so we
@@ -479,7 +444,7 @@
479
444
  }
480
445
  $sel.val(currentValue || '');
481
446
  })
482
- .fail(function () {
447
+ .fail(() => {
483
448
  // Admin route not reachable. Preserve the saved
484
449
  // value as an explicit option so the dropdown doesn't
485
450
  // silently overwrite persistStore with '' on save.
@@ -491,43 +456,67 @@
491
456
  $sel.val(currentValue);
492
457
  }
493
458
  });
494
- }
459
+ };
495
460
 
496
461
  // Last `size` handed to oneditresize. Cached so the <details>
497
462
  // toggle handler can re-run the resize math without waiting for
498
463
  // the next tray drag.
499
- var lastTraySize = null;
500
-
501
- // Stretch the Wait paths editableList to fill the tray. Mirrors
502
- // the pattern in Node-RED core (switch/change/httprequest):
503
- // subtract every other row's outer height from the tray height
504
- // and give the remainder to the list. The :visible filter mirrors
505
- // httprequest so hidden rows (e.g. timeout-hint) are skipped
506
- // explicitly. No upper clamp — when the user drags the tray
507
- // taller they want to see more rows.
508
- function resizePathsList() {
509
- var $form = $(SEL.dialogForm);
464
+ let lastTraySize = null;
465
+
466
+ // Stretch the Wait paths editableList to fill the tray
467
+ // pattern from core switch/change/httprequest.
468
+ const resizePathsList = () => {
469
+ const $form = $(SEL.dialogForm);
510
470
  if (!$form.length || !lastTraySize || !lastTraySize.height) return;
511
- var height = lastTraySize.height;
471
+ let height = lastTraySize.height;
472
+ // jQuery `.each` binds `this` to the row element, so this
473
+ // callback stays a function expression.
512
474
  $form.children(':not(.node-input-paths-container-row)').each(function () {
513
- var $r = $(this);
475
+ const $r = $(this);
514
476
  if ($r.is(':visible')) height -= $r.outerHeight(true) || 0;
515
477
  });
516
478
  height -= 12; // breathing room for margins/padding the loop misses
517
479
  if (height < 140) height = 140;
518
480
  $('#' + LIST_ID.paths).editableList('height', height);
519
- }
481
+ };
520
482
 
521
- function openExamplesDialog() {
522
- // The import dialog opens as a modal above the edit tray, so
523
- // the user can pick a flow and on close return to this edit
524
- // dialog with in-flight edits intact. The earlier approach of
525
- // closing the tray first was unreliable (close-callback drops
526
- // when stack races editor teardown).
527
- if (RED.actions && typeof RED.actions.invoke === 'function') {
528
- RED.actions.invoke('core:show-examples-import-dialog');
483
+ // clipboard.js disables import actions while an edit tray is
484
+ // open, and RED.tray.close(cb) fires cb before the editor:close
485
+ // event so we listen for editor:close ourselves and defer one
486
+ // tick past the synchronous listener chain that clears the flag.
487
+ // The pending guard suppresses a fast double-click registering
488
+ // two listeners.
489
+ let pendingExamplesOpen = false;
490
+ const openExamplesDialog = () => {
491
+ if (pendingExamplesOpen) return;
492
+ pendingExamplesOpen = true;
493
+ const invoke = () => {
494
+ setTimeout(() => {
495
+ pendingExamplesOpen = false;
496
+ if (!RED.actions || typeof RED.actions.invoke !== 'function') return;
497
+ // core:show-examples-import-dialog was added in Node-RED 3.1.
498
+ const actions = RED.actions.list && RED.actions.list();
499
+ const hasExamples =
500
+ Array.isArray(actions) &&
501
+ actions.some((a) => (a && a.id) === 'core:show-examples-import-dialog');
502
+ RED.actions.invoke(hasExamples ? 'core:show-examples-import-dialog' : 'core:show-import-dialog');
503
+ }, 0);
504
+ };
505
+ if (RED.events && typeof RED.events.once === 'function') {
506
+ RED.events.once('editor:close', invoke);
507
+ } else if (RED.events && typeof RED.events.on === 'function') {
508
+ const handler = () => {
509
+ if (RED.events.off) RED.events.off('editor:close', handler);
510
+ invoke();
511
+ };
512
+ RED.events.on('editor:close', handler);
529
513
  }
530
- }
514
+ if (RED.tray && typeof RED.tray.close === 'function') {
515
+ RED.tray.close();
516
+ } else {
517
+ invoke();
518
+ }
519
+ };
531
520
 
532
521
  RED.nodes.registerType('join-wait', {
533
522
  category: 'function',
@@ -554,10 +543,9 @@
554
543
  firstMsg: { value: 'true', required: true },
555
544
  mapPayload: { value: 'false', required: true },
556
545
  disableComplete: { value: false },
557
- // Defaults to true so the queue survives a redeploy out of
558
- // the box (uses the default in-memory context store).
559
- // For full Node-RED restart persistence the user still needs
560
- // to point Persist store at a configured persistent store.
546
+ // On by default survives redeploys via the in-memory
547
+ // context store. Restart-persistence still needs Persist
548
+ // store pointing at a persistent context.
561
549
  persistOnRestart: { value: true },
562
550
  persistStore: { value: '' },
563
551
  },
@@ -588,10 +576,7 @@
588
576
  'jsonata',
589
577
  ],
590
578
  });
591
- // Spinner accepts decimals (1.5 seconds, 0.25 hours, )
592
- // — the runtime multiplies the value by the unit factor,
593
- // so fractional units are valid. step:1 keeps the up/down
594
- // arrows snappy for the common integer case.
579
+ // Decimals OK (runtime multiplies by the unit factor).
595
580
  $(SEL.timeout).spinner({ min: 0.001, step: 1, numberFormat: 'n' });
596
581
 
597
582
  initEditableList(LIST_ID.paths, toArray(this.paths), {
@@ -613,23 +598,19 @@
613
598
 
614
599
  populatePersistStores(this.persistStore);
615
600
 
616
- // Hide the Persist store row when Preserve queue is off —
617
- // the override is meaningless without persistence enabled.
618
- var $persistRow = $(SEL.persistStoreRow + ', ' + SEL.persistStoreTip);
619
- function syncPersistStoreVisibility() {
601
+ // Persist store row is meaningless without Preserve queue.
602
+ const $persistRow = $(SEL.persistStoreRow + ', ' + SEL.persistStoreTip);
603
+ const syncPersistStoreVisibility = () => {
620
604
  $persistRow.toggle($(SEL.persistOnRestart).is(':checked'));
621
605
  resizePathsList();
622
- }
606
+ };
623
607
  $(SEL.persistOnRestart).on('change', syncPersistStoreVisibility);
624
608
  syncPersistStoreVisibility();
625
609
 
626
610
  $(SEL.examplesButton).on('click', openExamplesDialog);
627
611
 
628
- // Re-run the editableList resize math when Advanced is
629
- // expanded/collapsed — oneditresize only fires on tray
630
- // drag, so without this the Wait paths list keeps the
631
- // height it had before the toggle and doesn't reclaim
632
- // (or yield) space.
612
+ // Reflow the list when Advanced toggles (oneditresize
613
+ // only fires on tray drag).
633
614
  $(SEL.advancedDetails).on('toggle', resizePathsList);
634
615
  },
635
616
  oneditsave: function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-join-wait",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Node-RED node that joins related messages across multiple paths within a time window — with exact-order matching, regex paths, correlation grouping, reset paths, and queue persistence. Coordinate parallel flows, synchronize events, and debounce sensors.",
5
5
  "author": "Daniel Caspi <dan@element26.net>",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "main": "join-wait.js",
26
26
  "engines": {
27
- "node": ">=20"
27
+ "node": ">=18.5"
28
28
  },
29
29
  "files": [
30
30
  "join-wait.js",
@@ -63,8 +63,7 @@
63
63
  "version": ">=3.0.0",
64
64
  "nodes": {
65
65
  "join-wait": "join-wait.js"
66
- },
67
- "examples": "examples"
66
+ }
68
67
  },
69
68
  "dependencies": {
70
69
  "jsonata": "^2.0.5"