@theotherwillembotha/node-red-whatsapp 0.0.55 → 0.4.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.
Files changed (37) hide show
  1. package/README.md +33 -21
  2. package/build/GenerateNodes.d.ts +2 -0
  3. package/build/GenerateNodes.d.ts.map +1 -0
  4. package/build/GenerateNodes.js +5 -7
  5. package/build/Nodes.html +1522 -538
  6. package/build/Nodes.js +52261 -14
  7. package/build/Plugins.html +37 -0
  8. package/build/Plugins.js +45828 -55
  9. package/build/index.d.ts +7 -0
  10. package/build/index.d.ts.map +1 -0
  11. package/build/index.js +1 -0
  12. package/build/runtime/NodeManagerRuntime.js +140 -0
  13. package/build/whatsapp/node/WhatsappAccountConfigNode.d.ts +14 -0
  14. package/build/whatsapp/node/WhatsappAccountConfigNode.d.ts.map +1 -0
  15. package/build/whatsapp/node/WhatsappAccountConfigNode.js +7 -2
  16. package/build/whatsapp/node/WhatsappDynamicSendMessageNode.d.ts +19 -0
  17. package/build/whatsapp/node/WhatsappDynamicSendMessageNode.d.ts.map +1 -0
  18. package/build/whatsapp/node/WhatsappDynamicSendMessageNode.js +112 -0
  19. package/build/whatsapp/node/WhatsappGroupConfigNode.d.ts +16 -0
  20. package/build/whatsapp/node/WhatsappGroupConfigNode.d.ts.map +1 -0
  21. package/build/whatsapp/node/WhatsappGroupConfigNode.js +19 -8
  22. package/build/whatsapp/node/WhatsappReceiveMessageNode.d.ts +20 -0
  23. package/build/whatsapp/node/WhatsappReceiveMessageNode.d.ts.map +1 -0
  24. package/build/whatsapp/node/WhatsappReceiveMessageNode.js +2 -2
  25. package/build/whatsapp/node/WhatsappSendMessageNode.d.ts +17 -0
  26. package/build/whatsapp/node/WhatsappSendMessageNode.d.ts.map +1 -0
  27. package/build/whatsapp/node/WhatsappSendMessageNode.js +55 -33
  28. package/build/whatsapp/service/WhatsappClient.d.ts +189 -0
  29. package/build/whatsapp/service/WhatsappClient.d.ts.map +1 -0
  30. package/build/whatsapp/service/WhatsappClient.js +140 -14
  31. package/build/whatsapp/service/WhatsappService.d.ts +26 -0
  32. package/build/whatsapp/service/WhatsappService.d.ts.map +1 -0
  33. package/build/whatsapp/service/WhatsappService.js +85 -23
  34. package/build/whatsapp/service/WhatsappStore.d.ts +127 -0
  35. package/build/whatsapp/service/WhatsappStore.d.ts.map +1 -0
  36. package/build/whatsapp/service/WhatsappStore.js +10 -7
  37. package/package.json +11 -8
package/build/Nodes.html CHANGED
@@ -7,173 +7,418 @@
7
7
  You have been warned.
8
8
 
9
9
  -->
10
-
11
10
  <!-- WhatsappAccountConfigNode -->
11
+
12
+ <style>
13
+ .theotherwillembotha-compact-table li {
14
+ padding-top: 4px;
15
+ padding-bottom: 4px;
16
+ }
17
+ </style>
18
+
19
+
20
+ <script type="text/javascript">
21
+ // a simple client for interfaceing with the WhatsappService on the backend. Note: this needs to be moved to its own file.
22
+ let waService = {
23
+ unlinkAccount : function(nodeID, localConnectionId){
24
+ return new Promise((resolve, fail) => {
25
+
26
+ let request = JSON.stringify({
27
+ nodeID: nodeID,
28
+ localConnectionId: localConnectionId
29
+ });
30
+
31
+ $.ajax({
32
+ url: "whatsapp/unlinkaccount",
33
+ type: "POST",
34
+ data: request,
35
+ contentType: "application/json; charset=utf-8",
36
+ success: (response) => {
37
+ resolve(response);
38
+ },
39
+ error: (jqXHR, textStatus, errorThrown) => {
40
+ fail(errorThrown);
41
+ }
42
+ });
43
+ });
44
+ },
45
+
46
+ getConnectionDetails:function(nodeID, localConnectionId){
47
+ return new Promise((resolve, fail) => {
48
+ let request = JSON.stringify({
49
+ nodeID: nodeID,
50
+ localConnectionId: localConnectionId
51
+ });
52
+
53
+ $.ajax({
54
+ url: "whatsapp/connectiondetails",
55
+ type: "POST",
56
+ data: request,
57
+ contentType: "application/json; charset=utf-8",
58
+ success: (response) => {
59
+ resolve(response);
60
+ },
61
+ error: (jqXHR, textStatus, errorThrown) => {
62
+ fail(errorThrown);
63
+ }
64
+ });
65
+ });
66
+ },
67
+
68
+ createGroup:function(nodeID, localConnectionId, name){
69
+ console.log("create group! " + name);
70
+
71
+ return new Promise((resolve, fail) => {
72
+ let request = JSON.stringify({
73
+ nodeID: nodeID,
74
+ localConnectionId: localConnectionId,
75
+ groupName: name
76
+ });
77
+
78
+ $.ajax({
79
+ url: "whatsapp/creategroup",
80
+ type: "POST",
81
+ data: request,
82
+ contentType: "application/json; charset=utf-8",
83
+ success: (response) => {
84
+ resolve(response);
85
+ },
86
+ error: (jqXHR, textStatus, errorThrown) => {
87
+ fail(errorThrown);
88
+ }
89
+ });
90
+ });
91
+ },
92
+
93
+ getGroups:function(nodeID, localConnectionId){
94
+ return new Promise((resolve, fail) => {
95
+ let request = JSON.stringify({
96
+ nodeID: nodeID,
97
+ localConnectionId: localConnectionId
98
+ });
99
+
100
+ $.ajax({
101
+ url: "whatsapp/getgroups",
102
+ type: "POST",
103
+ data: request,
104
+ contentType: "application/json; charset=utf-8",
105
+ success: (response) => {
106
+ resolve(response);
107
+ },
108
+ error: (jqXHR, textStatus, errorThrown) => {
109
+ fail(errorThrown);
110
+ }
111
+ });
112
+ });
113
+ },
114
+
115
+ addUserToGroup:function(nodeID, localConnectionId, userId, groupId, role){
116
+ return new Promise((resolve, fail) => {
117
+ let request = JSON.stringify({
118
+ nodeID: nodeID,
119
+ localConnectionId: localConnectionId,
120
+ userId: userId,
121
+ groupId: groupId,
122
+ role:role
123
+ });
124
+
125
+ $.ajax({
126
+ url: "whatsapp/groupadduser",
127
+ type: "POST",
128
+ data: request,
129
+ contentType: "application/json; charset=utf-8",
130
+ success: (response) => {
131
+ resolve(response);
132
+ },
133
+ error: (jqXHR, textStatus, errorThrown) => {
134
+ fail(errorThrown);
135
+ }
136
+ });
137
+ });
138
+ },
139
+
140
+ updateUserInGroup:function(nodeID, localConnectionId, userId, groupId, role){
141
+ console.log("update user in group.");
142
+ return new Promise((resolve, fail) => {
143
+ let request = JSON.stringify({
144
+ nodeID: nodeID,
145
+ localConnectionId: localConnectionId,
146
+ userId: userId,
147
+ groupId: groupId,
148
+ role:role
149
+ });
150
+
151
+ $.ajax({
152
+ url: "whatsapp/groupupdateuser",
153
+ type: "POST",
154
+ data: request,
155
+ contentType: "application/json; charset=utf-8",
156
+ success: (response) => {
157
+ resolve(response);
158
+ },
159
+ error: (jqXHR, textStatus, errorThrown) => {
160
+ fail(errorThrown);
161
+ }
162
+ });
163
+ });
164
+
165
+ },
166
+
167
+ removeUserFromGroup:function(nodeID, localConnectionId, userId, groupId){
168
+ console.log("removeUserFromGroup", nodeID, localConnectionId, userId, groupId);
169
+ return new Promise((resolve, fail) => {
170
+ let request = JSON.stringify({
171
+ nodeID: nodeID,
172
+ localConnectionId: localConnectionId,
173
+ userId: userId,
174
+ groupId: groupId
175
+ });
176
+
177
+ $.ajax({
178
+ url: "whatsapp/groupremoveuser",
179
+ type: "POST",
180
+ data: request,
181
+ contentType: "application/json; charset=utf-8",
182
+ success: (response) => {
183
+ resolve(response);
184
+ },
185
+ error: (jqXHR, textStatus, errorThrown) => {
186
+ fail(errorThrown);
187
+ }
188
+ });
189
+ });
190
+ },
191
+
192
+ getAccounts: function() {
193
+ return new Promise((resolve, fail) => {
194
+ $.ajax({
195
+ url: "whatsapp/accounts",
196
+ type: "GET",
197
+ success: (response) => { resolve(response); },
198
+ error: (jqXHR, textStatus, errorThrown) => { fail(errorThrown); }
199
+ });
200
+ });
201
+ },
202
+ }
203
+ </script>
204
+
205
+
206
+ <!-- WhatsappGroupConfigNode -->
207
+
208
+ <style>
209
+
210
+ </style>
211
+
212
+
213
+ <!-- WhatsappSendMessageNode -->
214
+
215
+ <style>
216
+ .wa-send-action-fields .form-row { margin-bottom: 3px; }
217
+ </style>
218
+ <script>
219
+ var WA_SEND_TYPES = {
220
+ text: {
221
+ label: "Text",
222
+ icon: "fa-font",
223
+ fields: [
224
+ { key: "value", label: "Text", types: ["msg", "flow", "global", "str"] }
225
+ ]
226
+ },
227
+ image: {
228
+ label: "Image",
229
+ icon: "fa-image",
230
+ fields: [
231
+ { key: "value", label: "Image", types: ["msg", "flow", "global"] }
232
+ ]
233
+ },
234
+ video: {
235
+ label: "Video",
236
+ icon: "fa-video-camera",
237
+ fields: [
238
+ { key: "value", label: "Video", types: ["msg", "flow", "global"] }
239
+ ]
240
+ },
241
+ document: {
242
+ label: "Document",
243
+ icon: "fa-file",
244
+ fields: [
245
+ { key: "value", label: "File", types: ["msg", "flow", "global"] },
246
+ { key: "documentName", label: "Name", types: ["msg", "flow", "global", "str"] },
247
+ { key: "documentMimetype", label: "Mime Type", types: ["msg", "flow", "global", "str"] }
248
+ ]
249
+ }
250
+ };
251
+ </script>
252
+
253
+
254
+ <!-- logger -->
255
+
12
256
  <style>
13
- .theotherwillembotha-compact-table li {
14
- padding-top: 4px;
15
- padding-bottom: 4px;
257
+ .editorgroupborder {
258
+ border-width:1px;
259
+ border-style:solid;
260
+ border-color:lightgray;
261
+ padding: 3px;
262
+ margin-top: 2px;
263
+ margin-bottom: 2px;
264
+ }
265
+
266
+ .editorsectionheading {
267
+ font-weight: 600;
268
+ margin-bottom:1px !important;
269
+ margin-top:6px !important;
270
+ }
271
+
272
+ .nomargin {
273
+ margin-bottom:1px !important;
274
+ margin-top:1px !important;
275
+ }
276
+
277
+ .slidecontainer {
278
+ width: 100%;
279
+ }
280
+
281
+ .slider {
282
+ -webkit-appearance: none;
283
+ appearance: none;
284
+ width: 100%;
285
+ height: 25px;
286
+ background: #d3d3d3;
287
+ outline: none;
288
+ opacity: 0.7;
289
+ -webkit-transition: .2s;
290
+ transition: opacity .2s;
291
+ }
292
+
293
+ .slider:hover {
294
+ opacity: 1;
295
+ }
296
+
297
+ .slider::-webkit-slider-thumb {
298
+ -webkit-appearance: none;
299
+ appearance: none;
300
+ width: 25px;
301
+ height: 25px;
302
+ background: #04AA6D;
303
+ cursor: pointer;
304
+ }
305
+
306
+ .slider::-moz-range-thumb {
307
+ width: 25px;
308
+ height: 25px;
309
+ background: #04AA6D;
310
+ cursor: pointer;
311
+ }
312
+
313
+ .towb_editorlabel {
314
+ width: 120px !important;
315
+ }
316
+ input.towb_editorfield, select.towb_editorfield, textarea.towb_editorfield {
317
+ width: 70% !important;
318
+ }
319
+ input.towb_editorfield_short, select.towb_editorfield_short {
320
+ width: 30% !important;
16
321
  }
17
322
 
18
323
  </style>
324
+
19
325
  <script type="text/javascript">
20
- // a simple client for interfaceing with the WhatsappService on the backend. Note: this needs to be moved to its own file.
21
- let waService = {
22
- unlinkAccount: function(nodeID, localConnectionId) {
23
- return new Promise((resolve, fail) => {
24
- let request = JSON.stringify({
25
- nodeID: nodeID,
26
- localConnectionId: localConnectionId
27
- });
28
- $.ajax({
29
- url: "whatsapp/unlinkaccount",
30
- type: "POST",
31
- data: request,
32
- contentType: "application/json; charset=utf-8",
33
- success: (response) => {
34
- resolve(response);
35
- },
36
- error: (jqXHR, textStatus, errorThrown) => {
37
- fail(errorThrown);
38
- }
39
- });
40
- });
41
- },
42
- getConnectionDetails: function(nodeID, localConnectionId) {
43
- return new Promise((resolve, fail) => {
44
- let request = JSON.stringify({
45
- nodeID: nodeID,
46
- localConnectionId: localConnectionId
47
- });
48
- $.ajax({
49
- url: "whatsapp/connectiondetails",
50
- type: "POST",
51
- data: request,
52
- contentType: "application/json; charset=utf-8",
53
- success: (response) => {
54
- resolve(response);
55
- },
56
- error: (jqXHR, textStatus, errorThrown) => {
57
- fail(errorThrown);
58
- }
59
- });
60
- });
61
- },
62
- createGroup: function(nodeID, localConnectionId, name) {
63
- console.log("create group! " + name);
64
- return new Promise((resolve, fail) => {
65
- let request = JSON.stringify({
66
- nodeID: nodeID,
67
- localConnectionId: localConnectionId,
68
- groupName: name
69
- });
70
- $.ajax({
71
- url: "whatsapp/creategroup",
72
- type: "POST",
73
- data: request,
74
- contentType: "application/json; charset=utf-8",
75
- success: (response) => {
76
- resolve(response);
77
- },
78
- error: (jqXHR, textStatus, errorThrown) => {
79
- fail(errorThrown);
80
- }
81
- });
82
- });
83
- },
84
- getGroups: function(nodeID, localConnectionId) {
85
- return new Promise((resolve, fail) => {
86
- let request = JSON.stringify({
87
- nodeID: nodeID,
88
- localConnectionId: localConnectionId
89
- });
90
- $.ajax({
91
- url: "whatsapp/getgroups",
92
- type: "POST",
93
- data: request,
94
- contentType: "application/json; charset=utf-8",
95
- success: (response) => {
96
- resolve(response);
97
- },
98
- error: (jqXHR, textStatus, errorThrown) => {
99
- fail(errorThrown);
100
- }
101
- });
102
- });
103
- },
104
- addUserToGroup: function(nodeID, localConnectionId, userId, groupId, role) {
105
- return new Promise((resolve, fail) => {
106
- let request = JSON.stringify({
107
- nodeID: nodeID,
108
- localConnectionId: localConnectionId,
109
- userId: userId,
110
- groupId: groupId,
111
- role: role
112
- });
113
- $.ajax({
114
- url: "whatsapp/groupadduser",
115
- type: "POST",
116
- data: request,
117
- contentType: "application/json; charset=utf-8",
118
- success: (response) => {
119
- resolve(response);
326
+
327
+ // CSS
328
+ dropdownStyles = `
329
+ .loggertype-dropdown {
330
+ position: absolute;
331
+ background: white;
332
+ border: 1px solid #ccc;
333
+ border-radius: 4px;
334
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
335
+ max-height: 150px;
336
+ overflow-y: auto;
337
+ z-index: 1000;
338
+ min-width: 150px;
339
+ }
340
+ .loggertype-dropdown .dropdown-item {
341
+ padding: 6px 12px;
342
+ cursor: pointer;
343
+ }
344
+ .loggertype-dropdown .dropdown-item:hover {
345
+ background: #f0f0f0;
346
+ }
347
+ `;
348
+ $('<style>').text(dropdownStyles).appendTo('head');
349
+
350
+ </script>
351
+
352
+
353
+
354
+
355
+ <!-- WhatsappDynamicSendMessageNode -->
356
+
357
+ <style>
358
+ .wa-dynamic-send-action-fields .form-row { margin-bottom: 3px; }
359
+ </style>
360
+ <script>
361
+ // WA_SEND_TYPES is defined by WhatsappSendMessageNode.html (included in the same page).
362
+ // Guard against load-order variation with a var declaration — var re-declarations are safe.
363
+ if (typeof WA_SEND_TYPES === "undefined") {
364
+ var WA_SEND_TYPES = {
365
+ text: {
366
+ label: "Text",
367
+ icon: "fa-font",
368
+ fields: [
369
+ { key: "value", label: "Text", types: ["msg", "flow", "global", "str"] }
370
+ ]
120
371
  },
121
- error: (jqXHR, textStatus, errorThrown) => {
122
- fail(errorThrown);
123
- }
124
- });
125
- });
126
- },
127
- updateUserInGroup: function(nodeID, localConnectionId, userId, groupId, role) {
128
- console.log("update user in group.");
129
- return new Promise((resolve, fail) => {
130
- let request = JSON.stringify({
131
- nodeID: nodeID,
132
- localConnectionId: localConnectionId,
133
- userId: userId,
134
- groupId: groupId,
135
- role: role
136
- });
137
- $.ajax({
138
- url: "whatsapp/groupupdateuser",
139
- type: "POST",
140
- data: request,
141
- contentType: "application/json; charset=utf-8",
142
- success: (response) => {
143
- resolve(response);
372
+ image: {
373
+ label: "Image",
374
+ icon: "fa-image",
375
+ fields: [
376
+ { key: "value", label: "Image", types: ["msg", "flow", "global"] }
377
+ ]
144
378
  },
145
- error: (jqXHR, textStatus, errorThrown) => {
146
- fail(errorThrown);
147
- }
148
- });
149
- });
150
- },
151
- removeUserFromGroup: function(nodeID, localConnectionId, userId, groupId) {
152
- console.log("removeUserFromGroup", nodeID, localConnectionId, userId, groupId);
153
- return new Promise((resolve, fail) => {
154
- let request = JSON.stringify({
155
- nodeID: nodeID,
156
- localConnectionId: localConnectionId,
157
- userId: userId,
158
- groupId: groupId
159
- });
160
- $.ajax({
161
- url: "whatsapp/groupremoveuser",
162
- type: "POST",
163
- data: request,
164
- contentType: "application/json; charset=utf-8",
165
- success: (response) => {
166
- resolve(response);
379
+ video: {
380
+ label: "Video",
381
+ icon: "fa-video-camera",
382
+ fields: [
383
+ { key: "value", label: "Video", types: ["msg", "flow", "global"] }
384
+ ]
167
385
  },
168
- error: (jqXHR, textStatus, errorThrown) => {
169
- fail(errorThrown);
386
+ document: {
387
+ label: "Document",
388
+ icon: "fa-file",
389
+ fields: [
390
+ { key: "value", label: "File", types: ["msg", "flow", "global"] },
391
+ { key: "documentName", label: "Name", types: ["msg", "flow", "global", "str"] },
392
+ { key: "documentMimetype", label: "Mime Type", types: ["msg", "flow", "global", "str"] }
393
+ ]
170
394
  }
171
- });
172
- });
173
- },
395
+ };
174
396
  }
397
+ </script>
175
398
 
176
- </script>
399
+
400
+ <!-- WhatsappReceiveMessageNode -->
401
+
402
+ <style>
403
+ </style>
404
+ <script>
405
+ const WA_ACCEPT_FIELDS = [
406
+ { id: "Text", icon: "fa-font", label: "Text", defaultEnabled: true },
407
+ { id: "ExtendedText", icon: "fa-align-left", label: "Extended Text", defaultEnabled: true },
408
+ { id: "Image", icon: "fa-image", label: "Image", defaultEnabled: true },
409
+ { id: "Video", icon: "fa-video-camera", label: "Video", defaultEnabled: true },
410
+ { id: "Album", icon: "fa-th-large", label: "Album", defaultEnabled: true },
411
+ { id: "Document", icon: "fa-file", label: "Document", defaultEnabled: false },
412
+ { id: "Contact", icon: "fa-address-card", label: "Contact", defaultEnabled: false },
413
+ { id: "Template", icon: "fa-th-list", label: "Template", defaultEnabled: false },
414
+ { id: "Location", icon: "fa-map-marker", label: "Location", defaultEnabled: false },
415
+ { id: "Event", icon: "fa-calendar", label: "Event", defaultEnabled: false },
416
+ { id: "EventResponse", icon: "fa-calendar-check-o", label: "Event Response", defaultEnabled: false },
417
+ { id: "Sticker", icon: "fa-sticky-note-o", label: "Sticker", defaultEnabled: false },
418
+ ];
419
+ </script>
420
+
421
+
177
422
  <script type="text/javascript">
178
423
  RED.nodes.registerType('WhatsappAccountConfigNode', {
179
424
  category: 'config',
@@ -199,8 +444,9 @@
199
444
  {
200
445
  let node = this;
201
446
  let $btn = $("#wa-account-link-btn");
447
+ let isLinked = document.getElementById("node-config-input-accountLinked").checked;
202
448
  // if the account is already linked, show the Unlink state
203
- if (document.getElementById("node-config-input-accountLinked").checked) {
449
+ if (isLinked) {
204
450
  $btn.html('<i class="fa fa-unlink"></i> Unlink Account');
205
451
  }
206
452
  // if we don't have a local connectionId for this node, generate one.
@@ -208,10 +454,44 @@
208
454
  $("#node-config-input-localConnectionId").val(crypto.randomUUID());
209
455
  }
210
456
  let localConnectionId = $("#node-config-input-localConnectionId").val();
211
- $btn.on("click", () => {
457
+ // --- Claim Account section ---
458
+ // Only offer claiming if this node is not already linked.
459
+ if (!isLinked) {
460
+ waService.getAccounts().then(function(accounts) {
461
+ let unclaimed = accounts.filter(function(a) {
462
+ return !a.claimed;
463
+ });
464
+ if (unclaimed.length === 0) return;
465
+ let $claimSelect = $("#wa-claim-select");
466
+ $claimSelect.empty();
467
+ unclaimed.forEach(function(account) {
468
+ let label = account.name && account.phoneNumber ? account.name + " (+" + account.phoneNumber + ")" : account.name || account.phoneNumber || account.key;
469
+ if (!account.connected) label += " \u26a0 disconnected";
470
+ $('<option>').val(account.key).text(label).appendTo($claimSelect);
471
+ });
472
+ $("#wa-claim-section").show();
473
+ $("#wa-or-separator").show();
474
+ }).catch(function() {
475
+ /* backend not ready — ignore */ });
476
+ }
477
+ $("#wa-claim-btn").on("click", function() {
478
+ let claimedKey = $("#wa-claim-select").val();
479
+ if (!claimedKey) return;
480
+ // adopt the claimed account's localConnectionId and mark as linked
481
+ $("#node-config-input-localConnectionId").val(claimedKey);
482
+ $("#node-config-input-accountLinked").prop("checked", true);
483
+ $("#wa-claim-section").hide();
484
+ $("#wa-or-separator").hide();
485
+ $btn.html('<i class="fa fa-unlink"></i> Unlink Account');
486
+ $("#wa-account-qrcode").html("<p>\u2714 Account claimed. Deploy to activate.</p>");
487
+ });
488
+ // --- Link Account button ---
489
+ $btn.on("click", function() {
212
490
  $btn.prop("disabled", true);
491
+ // re-read localConnectionId in case it was overridden by a claim
492
+ localConnectionId = $("#node-config-input-localConnectionId").val();
213
493
  if (document.getElementById("node-config-input-accountLinked").checked) {
214
- waService.unlinkAccount(node.id, localConnectionId).then(response => {
494
+ waService.unlinkAccount(node.id, localConnectionId).then(function(response) {
215
495
  $btn.prop("disabled", false);
216
496
  $btn.html('<i class="fa fa-link"></i> Link Account');
217
497
  document.getElementById("node-config-input-accountLinked").checked = false;
@@ -283,6 +563,21 @@
283
563
  <input type="checkbox" id="node-config-input-accountLinked">
284
564
  </div>
285
565
 
566
+ <div class="form-row" id="wa-claim-section" style="display:none">
567
+ <label><i class="fa fa-mobile"></i> Claim</label>
568
+ <div style="width:70%; display:inline-flex; align-items:center;">
569
+ <select id="wa-claim-select" style="flex-grow:1; min-width:0;"></select>
570
+ <button type="button" id="wa-claim-btn" class="red-ui-button" style="margin-left:8px; flex-shrink:0;">
571
+ <i class="fa fa-check"></i> Claim
572
+ </button>
573
+ </div>
574
+ </div>
575
+
576
+ <div class="form-row" id="wa-or-separator" style="display:none">
577
+ <label></label>
578
+ <span style="color:#aaa; font-style:italic;">— or link a new account —</span>
579
+ </div>
580
+
286
581
  <div class="form-row">
287
582
  <label></label>
288
583
  <button type="button" id="wa-account-link-btn" class="red-ui-button">
@@ -318,9 +613,7 @@ To unlink the device, open the editor and click **Unlink Account**, then remove
318
613
 
319
614
  Credentials are persisted to `/data/whatsapp/<localConnectionId>/` if the `/data` directory exists (standard Node-RED Docker image), otherwise to `./whatsapp/<localConnectionId>/` relative to the Node-RED working directory.
320
615
  </script>
321
- <!-- WhatsappGroupConfigNode -->
322
- <style>
323
- </style>
616
+
324
617
  <script type="text/javascript">
325
618
  RED.nodes.registerType('WhatsappGroupConfigNode', {
326
619
  category: 'config',
@@ -591,8 +884,9 @@ Credentials are persisted to `/data/whatsapp/<localConnectionId>/` if the `/data
591
884
  options: []
592
885
  }]
593
886
  });
594
- // set initial button state
887
+ // set initial button state and load groups for the already-selected account
595
888
  setCreateButtonEnabled(accountConfigInput.val());
889
+ updateGroupList(accountConfigInput.val());
596
890
  // show inline create section
597
891
  $("#wa-group-new-btn").on("click", () => {
598
892
  $("#wa-group-new-row").hide();
@@ -705,8 +999,6 @@ Credentials are persisted to `/data/whatsapp/<localConnectionId>/` if the `/data
705
999
  </div>
706
1000
  </div>
707
1001
 
708
-
709
-
710
1002
  </div>
711
1003
  </script>
712
1004
 
@@ -725,119 +1017,7 @@ References a WhatsApp group within a linked account. Used as the target for Send
725
1017
 
726
1018
  Click **Create New Group** to create a new WhatsApp group from within Node-RED. The new group will appear in the group dropdown once created.
727
1019
  </script>
728
- <!-- WhatsappSendMessageNode -->
729
- <style>
730
- </style>
731
- <script>
732
- const WA_SEND_FIELDS = [{
733
- id: "text",
734
- icon: "fa-font",
735
- label: "Send Text",
736
- types: ["msg", "flow", "global", "str"]
737
- }, {
738
- id: "image",
739
- icon: "fa-image",
740
- label: "Send Image",
741
- types: ["msg", "flow", "global"]
742
- }, ];
743
-
744
- </script>
745
- <!-- logger -->
746
- <style>
747
- .editorgroupborder {
748
- border-width: 1px;
749
- border-style: solid;
750
- border-color: lightgray;
751
- padding: 3px;
752
- margin-top: 2px;
753
- margin-bottom: 2px;
754
- }
755
-
756
- .editorsectionheading {
757
- font-weight: 600;
758
- margin-bottom: 1px !important;
759
- margin-top: 6px !important;
760
- }
761
-
762
- .nomargin {
763
- margin-bottom: 1px !important;
764
- margin-top: 1px !important;
765
- }
766
-
767
- .slidecontainer {
768
- width: 100%;
769
- }
770
-
771
- .slider {
772
- -webkit-appearance: none;
773
- appearance: none;
774
- width: 100%;
775
- height: 25px;
776
- background: #d3d3d3;
777
- outline: none;
778
- opacity: 0.7;
779
- -webkit-transition: .2s;
780
- transition: opacity .2s;
781
- }
782
-
783
- .slider:hover {
784
- opacity: 1;
785
- }
786
-
787
- .slider::-webkit-slider-thumb {
788
- -webkit-appearance: none;
789
- appearance: none;
790
- width: 25px;
791
- height: 25px;
792
- background: #04AA6D;
793
- cursor: pointer;
794
- }
795
-
796
- .slider::-moz-range-thumb {
797
- width: 25px;
798
- height: 25px;
799
- background: #04AA6D;
800
- cursor: pointer;
801
- }
802
-
803
- .towb_editorlabel {
804
- width: 120px !important;
805
- }
806
-
807
- .towb_editorfield {
808
- width: 70% !important;
809
- }
810
-
811
- .towb_editorfield_short {
812
- width: 30% !important;
813
- }
814
-
815
- </style>
816
- <script type="text/javascript">
817
- // CSS
818
- dropdownStyles = `
819
- .loggertype-dropdown {
820
- position: absolute;
821
- background: white;
822
- border: 1px solid #ccc;
823
- border-radius: 4px;
824
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
825
- max-height: 150px;
826
- overflow-y: auto;
827
- z-index: 1000;
828
- min-width: 150px;
829
- }
830
- .loggertype-dropdown .dropdown-item {
831
- padding: 6px 12px;
832
- cursor: pointer;
833
- }
834
- .loggertype-dropdown .dropdown-item:hover {
835
- background: #f0f0f0;
836
- }
837
- `;
838
- $('<style>').text(dropdownStyles).appendTo('head');
839
1020
 
840
- </script>
841
1021
  <script type="text/javascript">
842
1022
  RED.nodes.registerType('WhatsappSendMessageNode', {
843
1023
  category: 'whatsapp',
@@ -865,10 +1045,8 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
865
1045
  required: false,
866
1046
  validate: function(value) {
867
1047
  try {
868
- var payloads = JSON.parse(value || "[]");
869
- return payloads.every(function(p) {
870
- return !p.enabled || (p.value !== undefined && p.value !== "");
871
- });
1048
+ JSON.parse(value || "[]");
1049
+ return true;
872
1050
  } catch (e) {
873
1051
  return false;
874
1052
  }
@@ -894,59 +1072,91 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
894
1072
  required: true
895
1073
  },
896
1074
  metricsReference: {
897
- type: 'MetricsConfigNode',
898
- required: false
1075
+ required: false,
1076
+ value: '',
1077
+ type: 'DelegatedConfigReferenceNode'
899
1078
  },
900
1079
  },
901
1080
  oneditprepare: function() {
902
1081
  // **** WhatsappSendMessageNode **** //
903
1082
  {
904
1083
  let node = this;
905
- let payloads = [];
1084
+ let actionList = $("#wa-send-actions-list");
1085
+ actionList.editableList({
1086
+ sortable: true,
1087
+ removable: true,
1088
+ height: "auto",
1089
+ scrollOnAdd: true,
1090
+ addButton: "Add Message",
1091
+ addItem: function(container, i, action) {
1092
+ action = action || {};
1093
+ let type = action.type || "text";
1094
+ container.css({
1095
+ padding: "6px 4px"
1096
+ });
1097
+ // ── Type row ──────────────────────────────────────────────────────
1098
+ let typeRow = $('<div style="display:flex; align-items:center; gap:8px; margin-bottom:5px;">');
1099
+ let iconEl = $('<i style="width:14px; text-align:center; flex-shrink:0;">').addClass("fa " + (WA_SEND_TYPES[type] ? WA_SEND_TYPES[type].icon : "fa-comment"));
1100
+ let typeSelect = $('<select class="wa-send-action-type" style="flex:0 0 auto;">');
1101
+ Object.keys(WA_SEND_TYPES).forEach(function(key) {
1102
+ typeSelect.append($("<option>").val(key).text(WA_SEND_TYPES[key].label));
1103
+ });
1104
+ typeSelect.val(type);
1105
+ typeRow.append(iconEl).append(typeSelect);
1106
+ container.append(typeRow);
1107
+ // ── Fields container ──────────────────────────────────────────────
1108
+ let fieldsContainer = $('<div class="wa-send-action-fields">');
1109
+ container.append(fieldsContainer);
1110
+ let renderFields = function(selectedType) {
1111
+ fieldsContainer.empty();
1112
+ let typeDef = WA_SEND_TYPES[selectedType];
1113
+ if (!typeDef) return;
1114
+ iconEl.removeClass().addClass("fa " + typeDef.icon);
1115
+ typeDef.fields.forEach(function(field) {
1116
+ let fieldRow = $('<div class="form-row nomargin" style="display:flex; align-items:center; gap:6px;">');
1117
+ fieldRow.append($('<label style="width:70px; margin:0; font-size:0.85em; flex-shrink:0;">').text(field.label));
1118
+ let input = $('<input type="text">').addClass("wa-send-field-" + field.key);
1119
+ fieldRow.append(input);
1120
+ fieldsContainer.append(fieldRow);
1121
+ input.typedInput({
1122
+ types: field.types
1123
+ }).typedInput("type", action[field.key + "Type"] || field.types[0]).typedInput("value", action[field.key] || "");
1124
+ fieldRow.find(".red-ui-typedInput-container").css({
1125
+ flex: "1",
1126
+ "min-width": "0",
1127
+ width: ""
1128
+ });
1129
+ });
1130
+ };
1131
+ renderFields(type);
1132
+ typeSelect.on("change", function() {
1133
+ // Reset saved values when the type changes so stale data isn't carried over.
1134
+ action = {
1135
+ type: $(this).val()
1136
+ };
1137
+ renderFields($(this).val());
1138
+ });
1139
+ }
1140
+ });
1141
+ let actions = [];
906
1142
  try {
907
- payloads = JSON.parse(node.payloads || "[]");
1143
+ actions = JSON.parse(node.payloads || "[]");
908
1144
  } catch (e) {}
909
- WA_SEND_FIELDS.forEach(function(field) {
910
- let saved = payloads.find(function(p) {
911
- return p.id === field.id;
912
- }) || {
913
- enabled: false,
914
- type: field.types[0],
915
- value: ""
916
- };
917
- let row = $('<div class="form-row nomargin">' + '<label style="width:120px;"><i class="fa ' + field.icon + '"></i> ' + field.label + '</label>' + '<div id="wa-send-row-' + field.id + '" style="display:inline-flex; align-items:center; gap:6px; vertical-align:middle; width:calc(100% - 128px);">' + '<input type="checkbox" id="wa-send-enabled-' + field.id + '" style="width:auto; margin:0; flex-shrink:0;">' + '<input type="text" id="wa-send-value-' + field.id + '">' + '</div>' + '</div>');
918
- $("#wa-send-payload-rows").append(row);
919
- $("#wa-send-value-" + field.id).typedInput({
920
- types: field.types
921
- }).typedInput("type", saved.type).typedInput("value", saved.value);
922
- // typedInput inserts its container as a sibling of the (now hidden) input, making it
923
- // a direct flex child — apply flex:1 so it expands to fill the remaining row width.
924
- $("#wa-send-row-" + field.id + " .red-ui-typedInput-container").css({
925
- "flex": "1",
926
- "min-width": "0",
927
- "width": ""
928
- });
929
- $("#wa-send-enabled-" + field.id).prop("checked", saved.enabled);
930
- // live validation: highlight the typedInput when checkbox is checked but value is empty.
931
- var checkFieldValid = function() {
932
- var enabled = $("#wa-send-enabled-" + field.id).prop("checked");
933
- var value = $("#wa-send-value-" + field.id).typedInput("value");
934
- var invalid = enabled && (!value || value === "");
935
- $("#wa-send-row-" + field.id + " .red-ui-typedInput-container").toggleClass("input-error", invalid);
936
- };
937
- $("#wa-send-enabled-" + field.id).on("change", checkFieldValid);
938
- $("#wa-send-value-" + field.id).on("change", checkFieldValid);
939
- checkFieldValid();
940
- });
1145
+ if (actions.length > 0) {
1146
+ actionList.editableList("addItems", actions);
1147
+ }
941
1148
  }
942
1149
  // **** logger **** //
943
1150
  {
944
1151
  let node = this;
1152
+ let templateConfig = {};
945
1153
  // controls.
946
1154
  let logEnabled = $("#node-input-logEnabled");
947
1155
  let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
948
1156
  let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
949
1157
  let logEnabledSection = $("#section_logEnabled");
1158
+ let loggerSection = $("#section_loggerconfig");
1159
+ loggerSection.hide();
950
1160
  // functions.
951
1161
  let getLoggerTypes = async function() {
952
1162
  let types = [];
@@ -1070,43 +1280,788 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
1070
1280
  loggerEditButton.removeClass("disabled")
1071
1281
  }
1072
1282
  });
1073
- // lastly, make sure that we set the correct value on the logger selector.
1074
- updateLoggerSelectorList().then(() => {
1075
- loggerSelector.val(node.logger);
1076
- loggerSelector.trigger('change');
1283
+ // Show the logger section only if at least one logger type is registered.
1284
+ // If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
1285
+ getLoggerTypes().then(loggerTypes => {
1286
+ if (loggerTypes.length === 0) return;
1287
+ loggerSection.show();
1288
+ updateLoggerSelectorList().then(() => {
1289
+ loggerSelector.val(node.logger);
1290
+ loggerSelector.trigger('change');
1291
+ });
1292
+ });
1293
+ }
1294
+ // **** metrics **** //
1295
+ {
1296
+ let node = this;
1297
+ let templateConfig = {};
1298
+ let metricsEnabled = $("#node-input-metricsEnabled");
1299
+ let metricsSection = $("#section_metricsconfig");
1300
+ let metricsContainer = metricsEnabled.closest(".editorgroupborder");
1301
+ let metricsEnabledSection = metricsContainer.find("#section_metricsEnabled");
1302
+ let metricsEnableCheck = metricsContainer.find(".metricsEnableCheck");
1303
+ metricsSection.hide();
1304
+ if (templateConfig.showBorder === false) {
1305
+ metricsSection.find(".editorsectionheading").hide();
1306
+ metricsSection.find(".editorgroupborder").css({
1307
+ border: "none",
1308
+ padding: 0,
1309
+ margin: 0
1310
+ });
1311
+ }
1312
+ if (templateConfig.showEnable === false) {
1313
+ metricsEnableCheck.hide();
1314
+ metricsEnabledSection.show();
1315
+ }
1316
+ let getMetricsProviderTypes = async function() {
1317
+ let types = [];
1318
+ await $.ajax({
1319
+ url: "nodetypeservice/find?tag=MetricsProvider",
1320
+ type: "GET",
1321
+ contentType: "application/json; charset=utf-8",
1322
+ success: (response) => {
1323
+ types = response;
1324
+ },
1325
+ error: (jqXHR, textStatus, errorThrown) => {
1326
+ console.log(jqXHR, textStatus, errorThrown);
1327
+ },
1328
+ });
1329
+ return types;
1330
+ };
1331
+ let getMetricsProviders = async () => {
1332
+ let providers = [];
1333
+ let providerTypes = (await getMetricsProviderTypes()).map(t => t.id);
1334
+ RED.nodes.eachConfig(cfg => {
1335
+ if (providerTypes.includes(cfg.type)) {
1336
+ providers.push({
1337
+ id: cfg.id,
1338
+ name: cfg.label(),
1339
+ type: cfg.type
1340
+ });
1341
+ }
1342
+ });
1343
+ return providers;
1344
+ };
1345
+ let updateMetricsSelectorList = async () => {
1346
+ metricsSelector.empty();
1347
+ (await getMetricsProviders()).forEach(provider => {
1348
+ $('<option value="' + provider.id + '">' + provider.name + '</option>').appendTo(metricsSelector);
1349
+ });
1350
+ $('<option value="_ADD_">none</option>').appendTo(metricsSelector);
1351
+ };
1352
+ metricsEnabled.on('change', function() {
1353
+ if (metricsEnabled.prop("checked")) {
1354
+ metricsEnabledSection.show();
1355
+ } else {
1356
+ metricsEnabledSection.hide();
1357
+ }
1358
+ });
1359
+ let metricsSelector = metricsContainer.find("#metrics-selector");
1360
+ let metricsEditButton = metricsContainer.find("#metrics-edit-btn");
1361
+ let metricsAddButton = metricsContainer.find("#metrics-add-btn");
1362
+ metricsEditButton.on('click', async function(event) {
1363
+ event.stopPropagation();
1364
+ if (metricsEditButton.hasClass("disabled")) return;
1365
+ let selectedProvider = (await getMetricsProviders()).find(p => p.id === metricsSelector.val());
1366
+ if (selectedProvider) {
1367
+ RED.editor.editConfig("#metrics-selector", selectedProvider.type, selectedProvider.id);
1368
+ }
1369
+ });
1370
+ metricsAddButton.on('click', function(event) {
1371
+ event.stopPropagation();
1372
+ let $btn = $(event.currentTarget);
1373
+ let btnOffset = $btn.offset();
1374
+ let btnH = $btn.outerHeight();
1375
+ let btnW = $btn.outerWidth();
1376
+ $('.metricstype-dropdown').remove();
1377
+ getMetricsProviderTypes().then(providerTypes => {
1378
+ let $dropdown = $('<div class="metricstype-dropdown red-ui-editor"></div>').css({
1379
+ position: 'absolute',
1380
+ zIndex: 1000
1381
+ });
1382
+ providerTypes.forEach(providerType => {
1383
+ $('<div class="dropdown-item"></div>').text(providerType.name).data('value', providerType.id).css({
1384
+ padding: '4px 8px',
1385
+ cursor: 'pointer'
1386
+ }).appendTo($dropdown);
1387
+ });
1388
+ $('body').append($dropdown);
1389
+ $dropdown.css({
1390
+ top: btnOffset.top + btnH,
1391
+ left: Math.max(0, btnOffset.left + btnW - $dropdown.outerWidth())
1392
+ });
1393
+ $(document).on('mousedown.metricsdropdown', function(e) {
1394
+ if (!$(e.target).closest('.metricstype-dropdown').length) {
1395
+ $dropdown.remove();
1396
+ $(document).off('mousedown.metricsdropdown');
1397
+ }
1398
+ });
1399
+ $dropdown.on('click', '.dropdown-item', function(e) {
1400
+ e.stopPropagation();
1401
+ const selectedValue = $(this).data('value');
1402
+ $dropdown.remove();
1403
+ RED.editor.editConfig("#metrics-selector", selectedValue, "_ADD_");
1404
+ });
1405
+ });
1406
+ });
1407
+ metricsSelector.on("focus", updateMetricsSelectorList);
1408
+ metricsSelector.on('change', function() {
1409
+ if ("_ADD_" === metricsSelector.val()) {
1410
+ metricsEditButton.addClass("disabled");
1411
+ } else {
1412
+ metricsEditButton.removeClass("disabled");
1413
+ }
1414
+ });
1415
+ // Show the metrics section only if at least one metrics provider type is registered.
1416
+ // If nodered_prometheus (or any other provider) is not installed, the section stays hidden.
1417
+ getMetricsProviderTypes().then(providerTypes => {
1418
+ if (providerTypes.length === 0) return;
1419
+ metricsSection.show();
1420
+ updateMetricsSelectorList().then(() => {
1421
+ metricsSelector.val(node.metricsReference);
1422
+ metricsSelector.trigger('change');
1423
+ });
1424
+ });
1425
+ }
1426
+ },
1427
+ oneditsave: function() {
1428
+ // **** WhatsappSendMessageNode **** //
1429
+ {
1430
+ let actions = [];
1431
+ $("#wa-send-actions-list").editableList("items").each(function() {
1432
+ let container = $(this);
1433
+ let type = container.find(".wa-send-action-type").val();
1434
+ let typeDef = WA_SEND_TYPES[type];
1435
+ if (!typeDef) return;
1436
+ let action = {
1437
+ type: type
1438
+ };
1439
+ typeDef.fields.forEach(function(field) {
1440
+ let input = container.find(".wa-send-field-" + field.key);
1441
+ if (input.length) {
1442
+ action[field.key] = input.typedInput("value");
1443
+ action[field.key + "Type"] = input.typedInput("type");
1444
+ }
1445
+ });
1446
+ actions.push(action);
1447
+ });
1448
+ $("#node-input-payloads").val(JSON.stringify(actions));
1449
+ }
1450
+ // **** logger **** //
1451
+ {
1452
+ let node = this;
1453
+ let templateConfig = {};
1454
+ node = this;
1455
+ var selectedLogger = $("#logger-selector").val();
1456
+ node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
1457
+ node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
1458
+ node.logTemplateOverrideEditor.destroy();
1459
+ delete node.logTemplateOverrideEditor;
1460
+ }
1461
+ // **** metrics **** //
1462
+ {
1463
+ let node = this;
1464
+ let templateConfig = {};
1465
+ node = this;
1466
+ var selectedProvider = $("#metrics-selector").val();
1467
+ node.metricsReference = (selectedProvider && selectedProvider !== "_ADD_") ? selectedProvider : '';
1468
+ if (templateConfig.showEnable === false) {
1469
+ node.metricsEnabled = !!node.metricsReference;
1470
+ }
1471
+ }
1472
+ },
1473
+ oneditcancel: function() {},
1474
+ oneditdelete: function() {},
1475
+ });
1476
+
1477
+ </script>
1478
+
1479
+
1480
+ <script type="text/html" data-template-name='WhatsappSendMessageNode'>
1481
+ <div id='section_WhatsappSendMessageNode'>
1482
+ <div class="form-row">
1483
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1484
+ <input type="text" id="node-input-name" />
1485
+ </div>
1486
+
1487
+ <div class="form-row nomargin">
1488
+ <label for="node-input-groupConfig" style="width:120px;"><i class="fa fa-users"></i> Group</label>
1489
+ <input type="text" id="node-input-groupConfig" placeholder="config">
1490
+ </div>
1491
+
1492
+ <div class="form-row editorsectionheading">
1493
+ <i class="w-16 fa fa-paper-plane"></i> <span>Messages</span>
1494
+ </div>
1495
+
1496
+ <div class="editorgroupborder">
1497
+ <ol id="wa-send-actions-list"></ol>
1498
+ </div>
1499
+
1500
+ <input type="hidden" id="node-input-payloads">
1501
+
1502
+
1503
+ </div>
1504
+
1505
+ <div id='section_logger'>
1506
+ <div id="section_loggerconfig">
1507
+ <div class="form-row editorsectionheading">
1508
+ <i class="w-16 fa fa-eye"></i> <span>Logging</span>
1509
+ </div>
1510
+ <div class="editorgroupborder">
1511
+ <div class="form-row nomargin logEnableCheck">
1512
+ <label class="towb_editorlabel">&nbsp;</label>
1513
+ <input class="towb_checkbox" type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false">
1514
+ <label class="towb_checkboxlabel" for="node-input-logEnabled"><span>Enable Logging</span></label>
1515
+ </div>
1516
+
1517
+ <div id="section_logEnabled">
1518
+ <div class="form-row nomargin">
1519
+ <label class="towb_editorlabel" for="logger-selector">Logger</label>
1520
+ <div style="width: 70%; display: inline-flex;">
1521
+ <select id="logger-selector" style="flex-grow: 1;" class="">
1522
+ <option value="_ADD_">none</option>
1523
+ </select>
1524
+ <a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
1525
+ <i class="fa fa-pencil"></i>
1526
+ </a>
1527
+ <a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
1528
+ <i class="fa fa-plus"></i>
1529
+ </a>
1530
+ </div>
1531
+ </div>
1532
+
1533
+ <div class="form-row nomargin">
1534
+ <label class="towb_editorlabel">&nbsp;</label>
1535
+ <input class="towb_checkbox" type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false">
1536
+ <label class="towb_checkboxlabel" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
1537
+ </div>
1538
+
1539
+ <div id="section_logTemplateOverrideEnabled">
1540
+ <div class="form-row">
1541
+ <label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
1542
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
1543
+ </div>
1544
+ </div>
1545
+ </div>
1546
+ </div>
1547
+ </div>
1548
+
1549
+ </div>
1550
+
1551
+ <div id='section_metrics'>
1552
+ <div id="section_metricsconfig">
1553
+ <div class="form-row editorsectionheading">
1554
+ <i class="w-16 fa fa-eye"></i> <span>Metrics</span>
1555
+ </div>
1556
+ <div class="editorgroupborder">
1557
+ <div class="form-row nomargin metricsEnableCheck">
1558
+ <label class="towb_editorlabel">&nbsp;</label>
1559
+ <input class="towb_checkbox" type="checkbox" id="node-input-metricsEnabled" value="false" />
1560
+ <label class="towb_checkboxlabel" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
1561
+ </div>
1562
+
1563
+ <div id="section_metricsEnabled">
1564
+ <div class="form-row nomargin">
1565
+ <label class="towb_editorlabel" for="metrics-selector">Metric Provider</label>
1566
+ <div style="width: 70%; display: inline-flex;">
1567
+ <select id="metrics-selector" style="flex-grow: 1; min-width: 0;">
1568
+ <option value="_ADD_">none</option>
1569
+ </select>
1570
+ <a id="metrics-edit-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;">
1571
+ <i class="fa fa-pencil"></i>
1572
+ </a>
1573
+ <a id="metrics-add-btn" class="red-ui-button" style="margin-left: 10px; flex-shrink: 0;">
1574
+ <i class="fa fa-plus"></i>
1575
+ </a>
1576
+ </div>
1577
+ </div>
1578
+ </div>
1579
+ </div>
1580
+ </div>
1581
+
1582
+ </div>
1583
+ </script>
1584
+
1585
+ <script type="text/markdown" data-help-name='WhatsappSendMessageNode'>
1586
+ > **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
1587
+
1588
+ Sends one or more messages to a WhatsApp group when triggered.
1589
+
1590
+ ### Properties
1591
+
1592
+ : *name* (string) : Display label for this node.
1593
+ : *group* (config) : The WhatsApp Group config node to send to.
1594
+
1595
+ ### Messages
1596
+
1597
+ Each row in the **Messages** list is sent as a separate WhatsApp message, in order, when the node is triggered. Rows can be reordered by dragging and removed with the delete button.
1598
+
1599
+ Select the message type from the dropdown, then fill in the value fields:
1600
+
1601
+ - **Text** — a single text value (`msg`, `flow`, `global`, `str` with Handlebars support for `str`)
1602
+ - **Image** — a Buffer containing the image data (`msg`, `flow`, `global`)
1603
+ - **Video** — a Buffer containing the video data (`msg`, `flow`, `global`)
1604
+ - **Document** — three fields: the file Buffer, an optional filename, and an optional MIME type (defaults to `application/octet-stream`)
1605
+
1606
+ ### Inputs
1607
+
1608
+ : *msg* : Trigger message. `msg`-type field values are resolved against this message.
1609
+
1610
+ ### Logging
1611
+ : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
1612
+ : *logger* (logconfig) : the endppoint that the node will log events to.
1613
+ : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
1614
+ : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
1615
+
1616
+ ### Metrics
1617
+ : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
1618
+ : *metric provider* (metricsconfig) : the metrics backend that this node will report to.
1619
+ </script>
1620
+
1621
+ <script type="text/javascript">
1622
+ RED.nodes.registerType('WhatsappDynamicSendMessageNode', {
1623
+ category: 'whatsapp',
1624
+ icon: 'whatsapp.png',
1625
+ color: '#E6FFDA',
1626
+ label: function() {
1627
+ return this.name
1628
+ },
1629
+ paletteLabel: 'WhatsApp Dynamic Send',
1630
+ inputs: 1,
1631
+ inputLabels: (i) => 'message',
1632
+ outputs: 0,
1633
+ outputLabels: (i) => [][i],
1634
+ defaults: {
1635
+ name: {
1636
+ value: 'Dynamic Send WhatsApp Message',
1637
+ required: true
1638
+ },
1639
+ accountConfig: {
1640
+ type: 'WhatsappAccountConfigNode',
1641
+ required: true
1642
+ },
1643
+ recipient: {
1644
+ value: 'payload.sender.id'
1645
+ },
1646
+ recipientType: {
1647
+ value: 'msg'
1648
+ },
1649
+ payloads: {
1650
+ value: '[]',
1651
+ required: false,
1652
+ validate: function(value) {
1653
+ try {
1654
+ JSON.parse(value || "[]");
1655
+ return true;
1656
+ } catch (e) {
1657
+ return false;
1658
+ }
1659
+ }
1660
+ },
1661
+ logEnabled: {
1662
+ value: false,
1663
+ required: true
1664
+ },
1665
+ logger: {
1666
+ required: false,
1667
+ value: '',
1668
+ type: 'DelegatedConfigReferenceNode'
1669
+ },
1670
+ logTemplateOverrideEnabled: {
1671
+ value: false
1672
+ },
1673
+ logTemplateOverride: {
1674
+ value: 'message:{{msg}}'
1675
+ },
1676
+ metricsEnabled: {
1677
+ value: false,
1678
+ required: true
1679
+ },
1680
+ metricsReference: {
1681
+ required: false,
1682
+ value: '',
1683
+ type: 'DelegatedConfigReferenceNode'
1684
+ },
1685
+ },
1686
+ oneditprepare: function() {
1687
+ // **** WhatsappDynamicSendMessageNode **** //
1688
+ {
1689
+ let node = this;
1690
+ // ── Recipient typed input ─────────────────────────────────────────────────
1691
+ $("#node-input-recipient").typedInput({
1692
+ types: ["msg", "flow", "global", "str"],
1693
+ typeField: "#node-input-recipientType"
1694
+ });
1695
+ // ── Messages editableList ─────────────────────────────────────────────────
1696
+ let actionList = $("#wa-dynamic-send-actions-list");
1697
+ actionList.editableList({
1698
+ sortable: true,
1699
+ removable: true,
1700
+ height: "auto",
1701
+ scrollOnAdd: true,
1702
+ addButton: "Add Message",
1703
+ addItem: function(container, i, action) {
1704
+ action = action || {};
1705
+ let type = action.type || "text";
1706
+ container.css({
1707
+ padding: "6px 4px"
1708
+ });
1709
+ // Type row
1710
+ let typeRow = $('<div style="display:flex; align-items:center; gap:8px; margin-bottom:5px;">');
1711
+ let iconEl = $('<i style="width:14px; text-align:center; flex-shrink:0;">').addClass("fa " + (WA_SEND_TYPES[type] ? WA_SEND_TYPES[type].icon : "fa-comment"));
1712
+ let typeSelect = $('<select class="wa-dynamic-send-action-type" style="flex:0 0 auto;">');
1713
+ Object.keys(WA_SEND_TYPES).forEach(function(key) {
1714
+ typeSelect.append($("<option>").val(key).text(WA_SEND_TYPES[key].label));
1715
+ });
1716
+ typeSelect.val(type);
1717
+ typeRow.append(iconEl).append(typeSelect);
1718
+ container.append(typeRow);
1719
+ // Fields container
1720
+ let fieldsContainer = $('<div class="wa-dynamic-send-action-fields">');
1721
+ container.append(fieldsContainer);
1722
+ let renderFields = function(selectedType) {
1723
+ fieldsContainer.empty();
1724
+ let typeDef = WA_SEND_TYPES[selectedType];
1725
+ if (!typeDef) return;
1726
+ iconEl.removeClass().addClass("fa " + typeDef.icon);
1727
+ typeDef.fields.forEach(function(field) {
1728
+ let fieldRow = $('<div class="form-row nomargin" style="display:flex; align-items:center; gap:6px;">');
1729
+ fieldRow.append($('<label style="width:70px; margin:0; font-size:0.85em; flex-shrink:0;">').text(field.label));
1730
+ let input = $('<input type="text">').addClass("wa-dynamic-send-field-" + field.key);
1731
+ fieldRow.append(input);
1732
+ fieldsContainer.append(fieldRow);
1733
+ input.typedInput({
1734
+ types: field.types
1735
+ }).typedInput("type", action[field.key + "Type"] || field.types[0]).typedInput("value", action[field.key] || "");
1736
+ fieldRow.find(".red-ui-typedInput-container").css({
1737
+ flex: "1",
1738
+ "min-width": "0",
1739
+ width: ""
1740
+ });
1741
+ });
1742
+ };
1743
+ renderFields(type);
1744
+ typeSelect.on("change", function() {
1745
+ action = {
1746
+ type: $(this).val()
1747
+ };
1748
+ renderFields($(this).val());
1749
+ });
1750
+ }
1751
+ });
1752
+ let actions = [];
1753
+ try {
1754
+ actions = JSON.parse(node.payloads || "[]");
1755
+ } catch (e) {}
1756
+ if (actions.length > 0) {
1757
+ actionList.editableList("addItems", actions);
1758
+ }
1759
+ }
1760
+ // **** logger **** //
1761
+ {
1762
+ let node = this;
1763
+ let templateConfig = {};
1764
+ // controls.
1765
+ let logEnabled = $("#node-input-logEnabled");
1766
+ let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
1767
+ let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
1768
+ let logEnabledSection = $("#section_logEnabled");
1769
+ let loggerSection = $("#section_loggerconfig");
1770
+ loggerSection.hide();
1771
+ // functions.
1772
+ let getLoggerTypes = async function() {
1773
+ let types = [];
1774
+ await $.ajax({
1775
+ url: "nodetypeservice/find?tag=LoggerType",
1776
+ type: "GET",
1777
+ contentType: "application/json; charset=utf-8",
1778
+ data: JSON.stringify({
1779
+ id: node.id
1780
+ }),
1781
+ success: (response) => {
1782
+ types = response;
1783
+ },
1784
+ error: (jqXHR, textStatus, errorThrown) => {
1785
+ console.log(jqXHR, textStatus, errorThrown);
1786
+ },
1787
+ });
1788
+ return types;
1789
+ }
1790
+ let getLoggers = async () => {
1791
+ let loggers = [];
1792
+ // get the list of types again.
1793
+ let loggerTypes = (await getLoggerTypes()).map(type => type.id);
1794
+ // add all of the known loggers.
1795
+ RED.nodes.eachConfig(cfg => {
1796
+ if (loggerTypes.includes(cfg.type)) {
1797
+ loggers.push({
1798
+ id: cfg.id,
1799
+ name: cfg.label(),
1800
+ type: cfg.type
1801
+ });
1802
+ }
1803
+ });
1804
+ return loggers;
1805
+ };
1806
+ let updateLoggerSelectorList = async () => {
1807
+ loggerSelector.empty();
1808
+ (await getLoggers()).forEach(logger => {
1809
+ $('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
1810
+ });
1811
+ $('<option value="_ADD_">none</option>').appendTo(loggerSelector);
1812
+ }
1813
+ logEnabled.on('change', function() {
1814
+ if (logEnabled.prop("checked")) {
1815
+ logEnabledSection.show();
1816
+ node._def.defaults.logger.required = true;
1817
+ } else {
1818
+ logEnabledSection.hide()
1819
+ node._def.defaults.logger.required = false;
1820
+ }
1821
+ });
1822
+ logTemplateEnabled.on('change', function() {
1823
+ if (logTemplateEnabled.prop("checked")) {
1824
+ logTemplateEnabledSection.show();
1825
+ node._def.defaults.logTemplateOverride.required = true;
1826
+ } else {
1827
+ logTemplateEnabledSection.hide()
1828
+ node._def.defaults.logTemplateOverride.required = false;
1829
+ }
1830
+ });
1831
+ node.logTemplateOverrideEditor = RED.editor.createEditor({
1832
+ id: 'node-input-logTemplateOverrideEditor',
1833
+ mode: 'ace/mode/handlebars',
1834
+ value: node.logTemplateOverride
1835
+ });
1836
+ let loggerSelector = $("#logger-selector");
1837
+ let loggerEditButton = $("#logger-edit-btn");
1838
+ let loggerAddButton = $("#logger-add-btn");
1839
+ loggerEditButton.on('click', async function(event) {
1840
+ event.stopPropagation();
1841
+ if (loggerEditButton.hasClass("disabled")) {
1842
+ return;
1843
+ }
1844
+ // figure out what type of logger this is.
1845
+ let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
1846
+ RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
1847
+ });
1848
+ loggerAddButton.on('click', function(event) {
1849
+ event.stopPropagation();
1850
+ let $btn = $(event.target);
1851
+ // Remove any existing dropdown
1852
+ $('.loggertype-dropdown').remove();
1853
+ getLoggerTypes().then(loggerTypes => {
1854
+ let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
1855
+ loggerTypes.forEach(loggerType => {
1856
+ $('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
1857
+ padding: '4px 8px',
1858
+ cursor: 'pointer'
1859
+ }).appendTo($dropdown);
1860
+ });
1861
+ // Append first so outerWidth() is measurable, then position so the
1862
+ // right edge of the dropdown aligns with the right edge of the button.
1863
+ $('body').append($dropdown);
1864
+ $dropdown.css({
1865
+ top: $btn.offset().top + $btn.outerHeight(),
1866
+ left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
1867
+ });
1868
+ $(document).on('mousedown.dropdown', function(e) {
1869
+ if (!$(e.target).closest('.loggertype-dropdown').length) {
1870
+ $dropdown.remove();
1871
+ $(document).off('mousedown.dropdown'); // Clean up the listener
1872
+ }
1873
+ });
1874
+ // Handle item selection
1875
+ $dropdown.on('click', '.dropdown-item', function(e) {
1876
+ e.stopPropagation();
1877
+ const selectedValue = $(this).data('value');
1878
+ const selectedText = $(this).text();
1879
+ // Do whatever you need with the selection
1880
+ console.log('Selected:', selectedValue, selectedText);
1881
+ $dropdown.remove();
1882
+ RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
1883
+ });
1884
+ })
1885
+ });
1886
+ loggerSelector.on("focus", updateLoggerSelectorList);
1887
+ loggerSelector.on('change', function(event) {
1888
+ if ("_ADD_" === loggerSelector.val()) {
1889
+ loggerEditButton.addClass("disabled")
1890
+ } else {
1891
+ loggerEditButton.removeClass("disabled")
1892
+ }
1893
+ });
1894
+ // Show the logger section only if at least one logger type is registered.
1895
+ // If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
1896
+ getLoggerTypes().then(loggerTypes => {
1897
+ if (loggerTypes.length === 0) return;
1898
+ loggerSection.show();
1899
+ updateLoggerSelectorList().then(() => {
1900
+ loggerSelector.val(node.logger);
1901
+ loggerSelector.trigger('change');
1902
+ });
1077
1903
  });
1078
1904
  }
1079
1905
  // **** metrics **** //
1080
1906
  {
1081
1907
  let node = this;
1908
+ let templateConfig = {};
1082
1909
  let metricsEnabled = $("#node-input-metricsEnabled");
1083
- let metricsEnabledSection = $("#section_metricsEnabled");
1910
+ let metricsSection = $("#section_metricsconfig");
1911
+ let metricsContainer = metricsEnabled.closest(".editorgroupborder");
1912
+ let metricsEnabledSection = metricsContainer.find("#section_metricsEnabled");
1913
+ let metricsEnableCheck = metricsContainer.find(".metricsEnableCheck");
1914
+ metricsSection.hide();
1915
+ if (templateConfig.showBorder === false) {
1916
+ metricsSection.find(".editorsectionheading").hide();
1917
+ metricsSection.find(".editorgroupborder").css({
1918
+ border: "none",
1919
+ padding: 0,
1920
+ margin: 0
1921
+ });
1922
+ }
1923
+ if (templateConfig.showEnable === false) {
1924
+ metricsEnableCheck.hide();
1925
+ metricsEnabledSection.show();
1926
+ }
1927
+ let getMetricsProviderTypes = async function() {
1928
+ let types = [];
1929
+ await $.ajax({
1930
+ url: "nodetypeservice/find?tag=MetricsProvider",
1931
+ type: "GET",
1932
+ contentType: "application/json; charset=utf-8",
1933
+ success: (response) => {
1934
+ types = response;
1935
+ },
1936
+ error: (jqXHR, textStatus, errorThrown) => {
1937
+ console.log(jqXHR, textStatus, errorThrown);
1938
+ },
1939
+ });
1940
+ return types;
1941
+ };
1942
+ let getMetricsProviders = async () => {
1943
+ let providers = [];
1944
+ let providerTypes = (await getMetricsProviderTypes()).map(t => t.id);
1945
+ RED.nodes.eachConfig(cfg => {
1946
+ if (providerTypes.includes(cfg.type)) {
1947
+ providers.push({
1948
+ id: cfg.id,
1949
+ name: cfg.label(),
1950
+ type: cfg.type
1951
+ });
1952
+ }
1953
+ });
1954
+ return providers;
1955
+ };
1956
+ let updateMetricsSelectorList = async () => {
1957
+ metricsSelector.empty();
1958
+ (await getMetricsProviders()).forEach(provider => {
1959
+ $('<option value="' + provider.id + '">' + provider.name + '</option>').appendTo(metricsSelector);
1960
+ });
1961
+ $('<option value="_ADD_">none</option>').appendTo(metricsSelector);
1962
+ };
1084
1963
  metricsEnabled.on('change', function() {
1085
1964
  if (metricsEnabled.prop("checked")) {
1086
1965
  metricsEnabledSection.show();
1087
- node._def.defaults.metricsReference.required = true;
1088
1966
  } else {
1089
- metricsEnabledSection.hide()
1090
- node._def.defaults.metricsReference.required = false;
1967
+ metricsEnabledSection.hide();
1968
+ }
1969
+ });
1970
+ let metricsSelector = metricsContainer.find("#metrics-selector");
1971
+ let metricsEditButton = metricsContainer.find("#metrics-edit-btn");
1972
+ let metricsAddButton = metricsContainer.find("#metrics-add-btn");
1973
+ metricsEditButton.on('click', async function(event) {
1974
+ event.stopPropagation();
1975
+ if (metricsEditButton.hasClass("disabled")) return;
1976
+ let selectedProvider = (await getMetricsProviders()).find(p => p.id === metricsSelector.val());
1977
+ if (selectedProvider) {
1978
+ RED.editor.editConfig("#metrics-selector", selectedProvider.type, selectedProvider.id);
1979
+ }
1980
+ });
1981
+ metricsAddButton.on('click', function(event) {
1982
+ event.stopPropagation();
1983
+ let $btn = $(event.currentTarget);
1984
+ let btnOffset = $btn.offset();
1985
+ let btnH = $btn.outerHeight();
1986
+ let btnW = $btn.outerWidth();
1987
+ $('.metricstype-dropdown').remove();
1988
+ getMetricsProviderTypes().then(providerTypes => {
1989
+ let $dropdown = $('<div class="metricstype-dropdown red-ui-editor"></div>').css({
1990
+ position: 'absolute',
1991
+ zIndex: 1000
1992
+ });
1993
+ providerTypes.forEach(providerType => {
1994
+ $('<div class="dropdown-item"></div>').text(providerType.name).data('value', providerType.id).css({
1995
+ padding: '4px 8px',
1996
+ cursor: 'pointer'
1997
+ }).appendTo($dropdown);
1998
+ });
1999
+ $('body').append($dropdown);
2000
+ $dropdown.css({
2001
+ top: btnOffset.top + btnH,
2002
+ left: Math.max(0, btnOffset.left + btnW - $dropdown.outerWidth())
2003
+ });
2004
+ $(document).on('mousedown.metricsdropdown', function(e) {
2005
+ if (!$(e.target).closest('.metricstype-dropdown').length) {
2006
+ $dropdown.remove();
2007
+ $(document).off('mousedown.metricsdropdown');
2008
+ }
2009
+ });
2010
+ $dropdown.on('click', '.dropdown-item', function(e) {
2011
+ e.stopPropagation();
2012
+ const selectedValue = $(this).data('value');
2013
+ $dropdown.remove();
2014
+ RED.editor.editConfig("#metrics-selector", selectedValue, "_ADD_");
2015
+ });
2016
+ });
2017
+ });
2018
+ metricsSelector.on("focus", updateMetricsSelectorList);
2019
+ metricsSelector.on('change', function() {
2020
+ if ("_ADD_" === metricsSelector.val()) {
2021
+ metricsEditButton.addClass("disabled");
2022
+ } else {
2023
+ metricsEditButton.removeClass("disabled");
1091
2024
  }
1092
2025
  });
2026
+ // Show the metrics section only if at least one metrics provider type is registered.
2027
+ // If nodered_prometheus (or any other provider) is not installed, the section stays hidden.
2028
+ getMetricsProviderTypes().then(providerTypes => {
2029
+ if (providerTypes.length === 0) return;
2030
+ metricsSection.show();
2031
+ updateMetricsSelectorList().then(() => {
2032
+ metricsSelector.val(node.metricsReference);
2033
+ metricsSelector.trigger('change');
2034
+ });
2035
+ });
1093
2036
  }
1094
2037
  },
1095
2038
  oneditsave: function() {
1096
- // **** WhatsappSendMessageNode **** //
2039
+ // **** WhatsappDynamicSendMessageNode **** //
1097
2040
  {
1098
- let payloads = WA_SEND_FIELDS.map(function(field) {
1099
- return {
1100
- id: field.id,
1101
- enabled: $("#wa-send-enabled-" + field.id).prop("checked"),
1102
- type: $("#wa-send-value-" + field.id).typedInput("type"),
1103
- value: $("#wa-send-value-" + field.id).typedInput("value"),
2041
+ let actions = [];
2042
+ $("#wa-dynamic-send-actions-list").editableList("items").each(function() {
2043
+ let container = $(this);
2044
+ let type = container.find(".wa-dynamic-send-action-type").val();
2045
+ let typeDef = WA_SEND_TYPES[type];
2046
+ if (!typeDef) return;
2047
+ let action = {
2048
+ type: type
1104
2049
  };
2050
+ typeDef.fields.forEach(function(field) {
2051
+ let input = container.find(".wa-dynamic-send-field-" + field.key);
2052
+ if (input.length) {
2053
+ action[field.key] = input.typedInput("value");
2054
+ action[field.key + "Type"] = input.typedInput("type");
2055
+ }
2056
+ });
2057
+ actions.push(action);
1105
2058
  });
1106
- $("#node-input-payloads").val(JSON.stringify(payloads));
2059
+ $("#node-input-payloads").val(JSON.stringify(actions));
1107
2060
  }
1108
2061
  // **** logger **** //
1109
2062
  {
2063
+ let node = this;
2064
+ let templateConfig = {};
1110
2065
  node = this;
1111
2066
  var selectedLogger = $("#logger-selector").val();
1112
2067
  node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
@@ -1115,7 +2070,16 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
1115
2070
  delete node.logTemplateOverrideEditor;
1116
2071
  }
1117
2072
  // **** metrics **** //
1118
- {}
2073
+ {
2074
+ let node = this;
2075
+ let templateConfig = {};
2076
+ node = this;
2077
+ var selectedProvider = $("#metrics-selector").val();
2078
+ node.metricsReference = (selectedProvider && selectedProvider !== "_ADD_") ? selectedProvider : '';
2079
+ if (templateConfig.showEnable === false) {
2080
+ node.metricsEnabled = !!node.metricsReference;
2081
+ }
2082
+ }
1119
2083
  },
1120
2084
  oneditcancel: function() {},
1121
2085
  oneditdelete: function() {},
@@ -1124,23 +2088,36 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
1124
2088
  </script>
1125
2089
 
1126
2090
 
1127
- <script type="text/html" data-template-name='WhatsappSendMessageNode'>
1128
- <div id='section_WhatsappSendMessageNode'>
2091
+ <script type="text/html" data-template-name='WhatsappDynamicSendMessageNode'>
2092
+ <div id='section_WhatsappDynamicSendMessageNode'>
1129
2093
  <div class="form-row">
1130
2094
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1131
2095
  <input type="text" id="node-input-name" />
1132
2096
  </div>
1133
2097
 
1134
- <div class="form-row nomargin">
1135
- <label for="node-input-groupConfig" style="width:120px;"><i class="fa fa-users"></i> Group</label>
1136
- <input type="text" id="node-input-groupConfig" placeholder="config">
2098
+ <div class="form-row editorsectionheading">
2099
+ <i class="w-16 fa fa-envelope"></i> <span>WhatsApp Account</span>
2100
+ </div>
2101
+
2102
+ <div class="editorgroupborder">
2103
+ <div class="form-row nomargin">
2104
+ <label for="node-input-accountConfig" style="width:120px;"><i class="fa fa-user"></i> Account</label>
2105
+ <input type="text" id="node-input-accountConfig" placeholder="config">
2106
+ </div>
2107
+
2108
+ <div class="form-row nomargin">
2109
+ <label for="node-input-recipient" style="width:120px;"><i class="fa fa-paper-plane"></i> Recipient</label>
2110
+ <input type="text" id="node-input-recipient" placeholder="payload.sender.id">
2111
+ <input type="hidden" id="node-input-recipientType">
2112
+ </div>
1137
2113
  </div>
1138
2114
 
1139
2115
  <div class="form-row editorsectionheading">
1140
- <i class="w-16 fa fa-paper-plane"></i> <span>Send</span>
2116
+ <i class="w-16 fa fa-paper-plane"></i> <span>Messages</span>
1141
2117
  </div>
1142
2118
 
1143
- <div class="editorgroupborder" id="wa-send-payload-rows">
2119
+ <div class="editorgroupborder">
2120
+ <ol id="wa-dynamic-send-actions-list"></ol>
1144
2121
  </div>
1145
2122
 
1146
2123
  <input type="hidden" id="node-input-payloads">
@@ -1155,13 +2132,14 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
1155
2132
  </div>
1156
2133
  <div class="editorgroupborder">
1157
2134
  <div class="form-row nomargin logEnableCheck">
1158
- <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
1159
- <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
2135
+ <label class="towb_editorlabel">&nbsp;</label>
2136
+ <input class="towb_checkbox" type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false">
2137
+ <label class="towb_checkboxlabel" for="node-input-logEnabled"><span>Enable Logging</span></label>
1160
2138
  </div>
1161
2139
 
1162
2140
  <div id="section_logEnabled">
1163
2141
  <div class="form-row nomargin">
1164
- <label for="logger-selector">Logger</label>
2142
+ <label class="towb_editorlabel" for="logger-selector">Logger</label>
1165
2143
  <div style="width: 70%; display: inline-flex;">
1166
2144
  <select id="logger-selector" style="flex-grow: 1;" class="">
1167
2145
  <option value="_ADD_">none</option>
@@ -1176,8 +2154,9 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
1176
2154
  </div>
1177
2155
 
1178
2156
  <div class="form-row nomargin">
1179
- <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
1180
- <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
2157
+ <label class="towb_editorlabel">&nbsp;</label>
2158
+ <input class="towb_checkbox" type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false">
2159
+ <label class="towb_checkboxlabel" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
1181
2160
  </div>
1182
2161
 
1183
2162
  <div id="section_logTemplateOverrideEnabled">
@@ -1199,14 +2178,25 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
1199
2178
  </div>
1200
2179
  <div class="editorgroupborder">
1201
2180
  <div class="form-row nomargin metricsEnableCheck">
1202
- <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
1203
- <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
2181
+ <label class="towb_editorlabel">&nbsp;</label>
2182
+ <input class="towb_checkbox" type="checkbox" id="node-input-metricsEnabled" value="false" />
2183
+ <label class="towb_checkboxlabel" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
1204
2184
  </div>
1205
2185
 
1206
2186
  <div id="section_metricsEnabled">
1207
2187
  <div class="form-row nomargin">
1208
- <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
1209
- <input id="node-input-metricsReference" placeholder="metrics" />
2188
+ <label class="towb_editorlabel" for="metrics-selector">Metric Provider</label>
2189
+ <div style="width: 70%; display: inline-flex;">
2190
+ <select id="metrics-selector" style="flex-grow: 1; min-width: 0;">
2191
+ <option value="_ADD_">none</option>
2192
+ </select>
2193
+ <a id="metrics-edit-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;">
2194
+ <i class="fa fa-pencil"></i>
2195
+ </a>
2196
+ <a id="metrics-add-btn" class="red-ui-button" style="margin-left: 10px; flex-shrink: 0;">
2197
+ <i class="fa fa-plus"></i>
2198
+ </a>
2199
+ </div>
1210
2200
  </div>
1211
2201
  </div>
1212
2202
  </div>
@@ -1215,26 +2205,28 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
1215
2205
  </div>
1216
2206
  </script>
1217
2207
 
1218
- <script type="text/markdown" data-help-name='WhatsappSendMessageNode'>
2208
+ <script type="text/markdown" data-help-name='WhatsappDynamicSendMessageNode'>
1219
2209
  > **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
1220
2210
 
1221
- Sends a message to a WhatsApp group.
2211
+ Sends one or more WhatsApp messages directly to a recipient resolved at runtime — for example, replying privately to the sender of a group message.
1222
2212
 
1223
2213
  ### Properties
1224
2214
 
1225
- : *name* (string) : Display label for this node.
1226
- : *group* (config) : The WhatsApp Group config node to send to.
2215
+ : *name* (string) : Display label for this node.
2216
+ : *account* (config) : The WhatsApp Account config node to send from.
2217
+ : *recipient* (typed) : The target JID resolved from the incoming message, flow, global context, or a static string. Defaults to `msg.payload.sender.id` — the JID of whoever sent the triggering message.
1227
2218
 
1228
- ### Send fields
2219
+ ### LID addressing
1229
2220
 
1230
- Each field has a checkbox to enable it and a typed value. Supported value types depend on the field:
2221
+ If the recipient JID ends with `@lid` (newer WhatsApp clients), the node automatically resolves it to the corresponding phone-number JID via the contact store before sending.
1231
2222
 
1232
- - **Send Text** — `msg`, `flow`, `global`, `str`
1233
- - **Send Image** — `msg`, `flow`, `global`
2223
+ ### Messages
2224
+
2225
+ Each row in the **Messages** list is sent as a separate WhatsApp message, in order. Supports Text, Image, Video, and Document (with optional filename and MIME type).
1234
2226
 
1235
2227
  ### Inputs
1236
2228
 
1237
- : *msg* : Trigger message. Field values are resolved against this message (for `msg` type) or from context.
2229
+ : *msg* : Trigger message. `msg`-type field values and the recipient are resolved against this message.
1238
2230
 
1239
2231
  ### Logging
1240
2232
  : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
@@ -1244,166 +2236,9 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1244
2236
 
1245
2237
  ### Metrics
1246
2238
  : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
1247
- : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
1248
- </script>
1249
- <!-- WhatsappReceiveMessageNode -->
1250
- <style>
1251
- </style>
1252
- <script>
1253
- const WA_ACCEPT_FIELDS = [{
1254
- id: "Text",
1255
- icon: "fa-font",
1256
- label: "Text",
1257
- defaultEnabled: true
1258
- }, {
1259
- id: "ExtendedText",
1260
- icon: "fa-align-left",
1261
- label: "Extended Text",
1262
- defaultEnabled: true
1263
- }, {
1264
- id: "Image",
1265
- icon: "fa-image",
1266
- label: "Image",
1267
- defaultEnabled: true
1268
- }, {
1269
- id: "Video",
1270
- icon: "fa-video-camera",
1271
- label: "Video",
1272
- defaultEnabled: true
1273
- }, {
1274
- id: "Album",
1275
- icon: "fa-th-large",
1276
- label: "Album",
1277
- defaultEnabled: true
1278
- }, {
1279
- id: "Document",
1280
- icon: "fa-file",
1281
- label: "Document",
1282
- defaultEnabled: false
1283
- }, {
1284
- id: "Contact",
1285
- icon: "fa-address-card",
1286
- label: "Contact",
1287
- defaultEnabled: false
1288
- }, {
1289
- id: "Template",
1290
- icon: "fa-th-list",
1291
- label: "Template",
1292
- defaultEnabled: false
1293
- }, {
1294
- id: "Interactive",
1295
- icon: "fa-hand-o-up",
1296
- label: "Interactive",
1297
- defaultEnabled: false
1298
- }, {
1299
- id: "Location",
1300
- icon: "fa-map-marker",
1301
- label: "Location",
1302
- defaultEnabled: false
1303
- }, {
1304
- id: "LiveLocation",
1305
- icon: "fa-location-arrow",
1306
- label: "Live Location",
1307
- defaultEnabled: false
1308
- }, ];
1309
-
2239
+ : *metric provider* (metricsconfig) : the metrics backend that this node will report to.
1310
2240
  </script>
1311
- <!-- logger -->
1312
- <style>
1313
- .editorgroupborder {
1314
- border-width: 1px;
1315
- border-style: solid;
1316
- border-color: lightgray;
1317
- padding: 3px;
1318
- margin-top: 2px;
1319
- margin-bottom: 2px;
1320
- }
1321
-
1322
- .editorsectionheading {
1323
- font-weight: 600;
1324
- margin-bottom: 1px !important;
1325
- margin-top: 6px !important;
1326
- }
1327
-
1328
- .nomargin {
1329
- margin-bottom: 1px !important;
1330
- margin-top: 1px !important;
1331
- }
1332
-
1333
- .slidecontainer {
1334
- width: 100%;
1335
- }
1336
-
1337
- .slider {
1338
- -webkit-appearance: none;
1339
- appearance: none;
1340
- width: 100%;
1341
- height: 25px;
1342
- background: #d3d3d3;
1343
- outline: none;
1344
- opacity: 0.7;
1345
- -webkit-transition: .2s;
1346
- transition: opacity .2s;
1347
- }
1348
-
1349
- .slider:hover {
1350
- opacity: 1;
1351
- }
1352
-
1353
- .slider::-webkit-slider-thumb {
1354
- -webkit-appearance: none;
1355
- appearance: none;
1356
- width: 25px;
1357
- height: 25px;
1358
- background: #04AA6D;
1359
- cursor: pointer;
1360
- }
1361
-
1362
- .slider::-moz-range-thumb {
1363
- width: 25px;
1364
- height: 25px;
1365
- background: #04AA6D;
1366
- cursor: pointer;
1367
- }
1368
-
1369
- .towb_editorlabel {
1370
- width: 120px !important;
1371
- }
1372
-
1373
- .towb_editorfield {
1374
- width: 70% !important;
1375
- }
1376
-
1377
- .towb_editorfield_short {
1378
- width: 30% !important;
1379
- }
1380
-
1381
- </style>
1382
- <script type="text/javascript">
1383
- // CSS
1384
- dropdownStyles = `
1385
- .loggertype-dropdown {
1386
- position: absolute;
1387
- background: white;
1388
- border: 1px solid #ccc;
1389
- border-radius: 4px;
1390
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1391
- max-height: 150px;
1392
- overflow-y: auto;
1393
- z-index: 1000;
1394
- min-width: 150px;
1395
- }
1396
- .loggertype-dropdown .dropdown-item {
1397
- padding: 6px 12px;
1398
- cursor: pointer;
1399
- }
1400
- .loggertype-dropdown .dropdown-item:hover {
1401
- background: #f0f0f0;
1402
- }
1403
- `;
1404
- $('<style>').text(dropdownStyles).appendTo('head');
1405
2241
 
1406
- </script>
1407
2242
  <script type="text/javascript">
1408
2243
  RED.nodes.registerType('WhatsappReceiveMessageNode', {
1409
2244
  category: 'whatsapp',
@@ -1458,8 +2293,9 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1458
2293
  required: true
1459
2294
  },
1460
2295
  metricsReference: {
1461
- type: 'MetricsConfigNode',
1462
- required: false
2296
+ required: false,
2297
+ value: '',
2298
+ type: 'DelegatedConfigReferenceNode'
1463
2299
  },
1464
2300
  },
1465
2301
  oneditprepare: function() {
@@ -1490,11 +2326,14 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1490
2326
  // **** logger **** //
1491
2327
  {
1492
2328
  let node = this;
2329
+ let templateConfig = {};
1493
2330
  // controls.
1494
2331
  let logEnabled = $("#node-input-logEnabled");
1495
2332
  let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
1496
2333
  let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
1497
2334
  let logEnabledSection = $("#section_logEnabled");
2335
+ let loggerSection = $("#section_loggerconfig");
2336
+ loggerSection.hide();
1498
2337
  // functions.
1499
2338
  let getLoggerTypes = async function() {
1500
2339
  let types = [];
@@ -1618,26 +2457,148 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1618
2457
  loggerEditButton.removeClass("disabled")
1619
2458
  }
1620
2459
  });
1621
- // lastly, make sure that we set the correct value on the logger selector.
1622
- updateLoggerSelectorList().then(() => {
1623
- loggerSelector.val(node.logger);
1624
- loggerSelector.trigger('change');
2460
+ // Show the logger section only if at least one logger type is registered.
2461
+ // If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
2462
+ getLoggerTypes().then(loggerTypes => {
2463
+ if (loggerTypes.length === 0) return;
2464
+ loggerSection.show();
2465
+ updateLoggerSelectorList().then(() => {
2466
+ loggerSelector.val(node.logger);
2467
+ loggerSelector.trigger('change');
2468
+ });
1625
2469
  });
1626
2470
  }
1627
2471
  // **** metrics **** //
1628
2472
  {
1629
2473
  let node = this;
2474
+ let templateConfig = {};
1630
2475
  let metricsEnabled = $("#node-input-metricsEnabled");
1631
- let metricsEnabledSection = $("#section_metricsEnabled");
2476
+ let metricsSection = $("#section_metricsconfig");
2477
+ let metricsContainer = metricsEnabled.closest(".editorgroupborder");
2478
+ let metricsEnabledSection = metricsContainer.find("#section_metricsEnabled");
2479
+ let metricsEnableCheck = metricsContainer.find(".metricsEnableCheck");
2480
+ metricsSection.hide();
2481
+ if (templateConfig.showBorder === false) {
2482
+ metricsSection.find(".editorsectionheading").hide();
2483
+ metricsSection.find(".editorgroupborder").css({
2484
+ border: "none",
2485
+ padding: 0,
2486
+ margin: 0
2487
+ });
2488
+ }
2489
+ if (templateConfig.showEnable === false) {
2490
+ metricsEnableCheck.hide();
2491
+ metricsEnabledSection.show();
2492
+ }
2493
+ let getMetricsProviderTypes = async function() {
2494
+ let types = [];
2495
+ await $.ajax({
2496
+ url: "nodetypeservice/find?tag=MetricsProvider",
2497
+ type: "GET",
2498
+ contentType: "application/json; charset=utf-8",
2499
+ success: (response) => {
2500
+ types = response;
2501
+ },
2502
+ error: (jqXHR, textStatus, errorThrown) => {
2503
+ console.log(jqXHR, textStatus, errorThrown);
2504
+ },
2505
+ });
2506
+ return types;
2507
+ };
2508
+ let getMetricsProviders = async () => {
2509
+ let providers = [];
2510
+ let providerTypes = (await getMetricsProviderTypes()).map(t => t.id);
2511
+ RED.nodes.eachConfig(cfg => {
2512
+ if (providerTypes.includes(cfg.type)) {
2513
+ providers.push({
2514
+ id: cfg.id,
2515
+ name: cfg.label(),
2516
+ type: cfg.type
2517
+ });
2518
+ }
2519
+ });
2520
+ return providers;
2521
+ };
2522
+ let updateMetricsSelectorList = async () => {
2523
+ metricsSelector.empty();
2524
+ (await getMetricsProviders()).forEach(provider => {
2525
+ $('<option value="' + provider.id + '">' + provider.name + '</option>').appendTo(metricsSelector);
2526
+ });
2527
+ $('<option value="_ADD_">none</option>').appendTo(metricsSelector);
2528
+ };
1632
2529
  metricsEnabled.on('change', function() {
1633
2530
  if (metricsEnabled.prop("checked")) {
1634
2531
  metricsEnabledSection.show();
1635
- node._def.defaults.metricsReference.required = true;
1636
2532
  } else {
1637
- metricsEnabledSection.hide()
1638
- node._def.defaults.metricsReference.required = false;
2533
+ metricsEnabledSection.hide();
2534
+ }
2535
+ });
2536
+ let metricsSelector = metricsContainer.find("#metrics-selector");
2537
+ let metricsEditButton = metricsContainer.find("#metrics-edit-btn");
2538
+ let metricsAddButton = metricsContainer.find("#metrics-add-btn");
2539
+ metricsEditButton.on('click', async function(event) {
2540
+ event.stopPropagation();
2541
+ if (metricsEditButton.hasClass("disabled")) return;
2542
+ let selectedProvider = (await getMetricsProviders()).find(p => p.id === metricsSelector.val());
2543
+ if (selectedProvider) {
2544
+ RED.editor.editConfig("#metrics-selector", selectedProvider.type, selectedProvider.id);
2545
+ }
2546
+ });
2547
+ metricsAddButton.on('click', function(event) {
2548
+ event.stopPropagation();
2549
+ let $btn = $(event.currentTarget);
2550
+ let btnOffset = $btn.offset();
2551
+ let btnH = $btn.outerHeight();
2552
+ let btnW = $btn.outerWidth();
2553
+ $('.metricstype-dropdown').remove();
2554
+ getMetricsProviderTypes().then(providerTypes => {
2555
+ let $dropdown = $('<div class="metricstype-dropdown red-ui-editor"></div>').css({
2556
+ position: 'absolute',
2557
+ zIndex: 1000
2558
+ });
2559
+ providerTypes.forEach(providerType => {
2560
+ $('<div class="dropdown-item"></div>').text(providerType.name).data('value', providerType.id).css({
2561
+ padding: '4px 8px',
2562
+ cursor: 'pointer'
2563
+ }).appendTo($dropdown);
2564
+ });
2565
+ $('body').append($dropdown);
2566
+ $dropdown.css({
2567
+ top: btnOffset.top + btnH,
2568
+ left: Math.max(0, btnOffset.left + btnW - $dropdown.outerWidth())
2569
+ });
2570
+ $(document).on('mousedown.metricsdropdown', function(e) {
2571
+ if (!$(e.target).closest('.metricstype-dropdown').length) {
2572
+ $dropdown.remove();
2573
+ $(document).off('mousedown.metricsdropdown');
2574
+ }
2575
+ });
2576
+ $dropdown.on('click', '.dropdown-item', function(e) {
2577
+ e.stopPropagation();
2578
+ const selectedValue = $(this).data('value');
2579
+ $dropdown.remove();
2580
+ RED.editor.editConfig("#metrics-selector", selectedValue, "_ADD_");
2581
+ });
2582
+ });
2583
+ });
2584
+ metricsSelector.on("focus", updateMetricsSelectorList);
2585
+ metricsSelector.on('change', function() {
2586
+ if ("_ADD_" === metricsSelector.val()) {
2587
+ metricsEditButton.addClass("disabled");
2588
+ } else {
2589
+ metricsEditButton.removeClass("disabled");
1639
2590
  }
1640
2591
  });
2592
+ // Show the metrics section only if at least one metrics provider type is registered.
2593
+ // If nodered_prometheus (or any other provider) is not installed, the section stays hidden.
2594
+ getMetricsProviderTypes().then(providerTypes => {
2595
+ if (providerTypes.length === 0) return;
2596
+ metricsSection.show();
2597
+ updateMetricsSelectorList().then(() => {
2598
+ metricsSelector.val(node.metricsReference);
2599
+ metricsSelector.trigger('change');
2600
+ });
2601
+ });
1641
2602
  }
1642
2603
  },
1643
2604
  oneditsave: function() {
@@ -1653,6 +2614,8 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1653
2614
  }
1654
2615
  // **** logger **** //
1655
2616
  {
2617
+ let node = this;
2618
+ let templateConfig = {};
1656
2619
  node = this;
1657
2620
  var selectedLogger = $("#logger-selector").val();
1658
2621
  node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
@@ -1661,7 +2624,16 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1661
2624
  delete node.logTemplateOverrideEditor;
1662
2625
  }
1663
2626
  // **** metrics **** //
1664
- {}
2627
+ {
2628
+ let node = this;
2629
+ let templateConfig = {};
2630
+ node = this;
2631
+ var selectedProvider = $("#metrics-selector").val();
2632
+ node.metricsReference = (selectedProvider && selectedProvider !== "_ADD_") ? selectedProvider : '';
2633
+ if (templateConfig.showEnable === false) {
2634
+ node.metricsEnabled = !!node.metricsReference;
2635
+ }
2636
+ }
1665
2637
  },
1666
2638
  oneditcancel: function() {},
1667
2639
  oneditdelete: function() {},
@@ -1720,13 +2692,14 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1720
2692
  </div>
1721
2693
  <div class="editorgroupborder">
1722
2694
  <div class="form-row nomargin logEnableCheck">
1723
- <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
1724
- <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
2695
+ <label class="towb_editorlabel">&nbsp;</label>
2696
+ <input class="towb_checkbox" type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false">
2697
+ <label class="towb_checkboxlabel" for="node-input-logEnabled"><span>Enable Logging</span></label>
1725
2698
  </div>
1726
2699
 
1727
2700
  <div id="section_logEnabled">
1728
2701
  <div class="form-row nomargin">
1729
- <label for="logger-selector">Logger</label>
2702
+ <label class="towb_editorlabel" for="logger-selector">Logger</label>
1730
2703
  <div style="width: 70%; display: inline-flex;">
1731
2704
  <select id="logger-selector" style="flex-grow: 1;" class="">
1732
2705
  <option value="_ADD_">none</option>
@@ -1741,8 +2714,9 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1741
2714
  </div>
1742
2715
 
1743
2716
  <div class="form-row nomargin">
1744
- <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
1745
- <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
2717
+ <label class="towb_editorlabel">&nbsp;</label>
2718
+ <input class="towb_checkbox" type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false">
2719
+ <label class="towb_checkboxlabel" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
1746
2720
  </div>
1747
2721
 
1748
2722
  <div id="section_logTemplateOverrideEnabled">
@@ -1764,14 +2738,25 @@ Each field has a checkbox to enable it and a typed value. Supported value types
1764
2738
  </div>
1765
2739
  <div class="editorgroupborder">
1766
2740
  <div class="form-row nomargin metricsEnableCheck">
1767
- <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
1768
- <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
2741
+ <label class="towb_editorlabel">&nbsp;</label>
2742
+ <input class="towb_checkbox" type="checkbox" id="node-input-metricsEnabled" value="false" />
2743
+ <label class="towb_checkboxlabel" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
1769
2744
  </div>
1770
2745
 
1771
2746
  <div id="section_metricsEnabled">
1772
2747
  <div class="form-row nomargin">
1773
- <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
1774
- <input id="node-input-metricsReference" placeholder="metrics" />
2748
+ <label class="towb_editorlabel" for="metrics-selector">Metric Provider</label>
2749
+ <div style="width: 70%; display: inline-flex;">
2750
+ <select id="metrics-selector" style="flex-grow: 1; min-width: 0;">
2751
+ <option value="_ADD_">none</option>
2752
+ </select>
2753
+ <a id="metrics-edit-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;">
2754
+ <i class="fa fa-pencil"></i>
2755
+ </a>
2756
+ <a id="metrics-add-btn" class="red-ui-button" style="margin-left: 10px; flex-shrink: 0;">
2757
+ <i class="fa fa-plus"></i>
2758
+ </a>
2759
+ </div>
1775
2760
  </div>
1776
2761
  </div>
1777
2762
  </div>
@@ -1808,7 +2793,6 @@ Each message type can be individually enabled or disabled. By default Text, Exte
1808
2793
 
1809
2794
  ### Metrics
1810
2795
  : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
1811
- : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
2796
+ : *metric provider* (metricsconfig) : the metrics backend that this node will report to.
1812
2797
  </script>
1813
-
1814
2798