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.
- package/CHANGELOG.md +28 -0
- package/join-wait.html +172 -191
- 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
|
-
//
|
|
143
|
-
|
|
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
|
-
|
|
163
|
+
const LIST_ID = {
|
|
167
164
|
paths: 'node-input-paths-container',
|
|
168
165
|
expire: 'node-input-pathsToExpire-container',
|
|
169
166
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
//
|
|
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:
|
|
320
|
-
|
|
296
|
+
addItem: (row, index, data) => {
|
|
297
|
+
const value = (data && data.value) || '';
|
|
321
298
|
row.css({ overflow: 'hidden', whiteSpace: 'nowrap' });
|
|
322
|
-
|
|
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',
|
|
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',
|
|
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(
|
|
314
|
+
setTimeout(() => {
|
|
338
315
|
$container.editableList('items').last().find('input').trigger('focus');
|
|
339
316
|
}, 0);
|
|
340
317
|
}
|
|
341
318
|
});
|
|
342
|
-
// Bulk-paste
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
324
|
+
const text = clip.getData('text');
|
|
351
325
|
if (!text || !/[\r\n]/.test(text)) return;
|
|
352
|
-
|
|
326
|
+
const lines = text
|
|
353
327
|
.split(/\r?\n/)
|
|
354
|
-
.map(
|
|
355
|
-
|
|
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 (
|
|
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:
|
|
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(
|
|
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
|
-
|
|
357
|
+
const rebindRegexValidation = () => {
|
|
388
358
|
$(SEL.useRegex).on('change', validateAllRows);
|
|
389
|
-
}
|
|
359
|
+
};
|
|
390
360
|
|
|
391
|
-
|
|
392
|
-
|
|
361
|
+
const readEditableList = (containerId) => {
|
|
362
|
+
const values = [];
|
|
393
363
|
$('#' + containerId)
|
|
394
364
|
.editableList('items')
|
|
395
365
|
.each(function () {
|
|
396
|
-
|
|
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
|
-
|
|
375
|
+
const toArray = (v) => {
|
|
404
376
|
if (Array.isArray(v)) return v;
|
|
405
377
|
if (typeof v === 'string' && v !== '') {
|
|
406
378
|
try {
|
|
407
|
-
|
|
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
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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(
|
|
467
|
-
(Array.isArray(stores) ? stores : []).forEach(
|
|
431
|
+
.done((stores) => {
|
|
432
|
+
(Array.isArray(stores) ? stores : []).forEach((s) => {
|
|
468
433
|
if (s.name === 'default') return; // already represented as the blank option
|
|
469
|
-
|
|
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(
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
// Stretch the Wait paths editableList to fill the tray
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
//
|
|
629
|
-
//
|
|
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.
|
|
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": ">=
|
|
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"
|