@flowfuse/node-red-dashboard 1.20.1 → 1.20.2-357ebba-202412181736.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 (69) hide show
  1. package/nodes/config/ui_base.js +138 -27
  2. package/nodes/store/data.js +15 -1
  3. package/nodes/widgets/locales/en-US/ui_audio.html +31 -0
  4. package/nodes/widgets/locales/en-US/ui_audio.json +17 -0
  5. package/nodes/widgets/ui_audio.html +113 -0
  6. package/nodes/widgets/ui_audio.js +81 -0
  7. package/nodes/widgets/ui_file_input.js +1 -1
  8. package/package.json +2 -1
  9. package/dist/apple-touch-icon.png +0 -0
  10. package/dist/assets/Tableau10-B-NsZVaP.js +0 -1
  11. package/dist/assets/array-BKyUJesY.js +0 -1
  12. package/dist/assets/blockDiagram-38ab4fdb-CMxXCB2U.js +0 -118
  13. package/dist/assets/c4Diagram-3d4e48cf-Cp5hSJnl.js +0 -10
  14. package/dist/assets/channel-DNOEREZ5.js +0 -1
  15. package/dist/assets/classDiagram-70f12bd4-B3RxdJz8.js +0 -2
  16. package/dist/assets/classDiagram-v2-f2320105-8HvDjcCx.js +0 -2
  17. package/dist/assets/clone-DUx8NCf1.js +0 -1
  18. package/dist/assets/createText-2e5e7dd3-Dd_JTvu4.js +0 -7
  19. package/dist/assets/disconnected-BuxohaUu.png +0 -0
  20. package/dist/assets/edges-e0da2a9e-DOHGlYkB.js +0 -4
  21. package/dist/assets/erDiagram-9861fffd-yOdzCO98.js +0 -51
  22. package/dist/assets/flowDb-956e92f1-CBOVQW-G.js +0 -10
  23. package/dist/assets/flowDiagram-66a62f08-DUeZEotM.js +0 -4
  24. package/dist/assets/flowDiagram-v2-96b9c2cf-dkHS5k1P.js +0 -1
  25. package/dist/assets/flowchart-elk-definition-4a651766-DiHGz-_a.js +0 -139
  26. package/dist/assets/ganttDiagram-c361ad54-DVPcSn9h.js +0 -257
  27. package/dist/assets/gitGraphDiagram-72cf32ee-BxXHXf9m.js +0 -70
  28. package/dist/assets/graph-D1W14WGM.js +0 -1
  29. package/dist/assets/index-3862675e-Czbct74y.js +0 -1
  30. package/dist/assets/index-BbWwSxsY.js +0 -250
  31. package/dist/assets/index-MyTOKb8y.css +0 -13
  32. package/dist/assets/infoDiagram-f8f76790-Dx3Iio-0.js +0 -7
  33. package/dist/assets/init-Gi6I4Gst.js +0 -1
  34. package/dist/assets/journeyDiagram-49397b02-wHSLlApM.js +0 -139
  35. package/dist/assets/katex-CvgdMzdh.js +0 -261
  36. package/dist/assets/layout-BpRCMw1H.js +0 -1
  37. package/dist/assets/line-DDza7Wzt.js +0 -1
  38. package/dist/assets/linear-CJJq30CP.js +0 -1
  39. package/dist/assets/logo-DIAzbBuw.png +0 -0
  40. package/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  41. package/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  42. package/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  43. package/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  44. package/dist/assets/mindmap-definition-fc14e90a-BIT01dqm.js +0 -110
  45. package/dist/assets/ordinal-Cboi1Yqb.js +0 -1
  46. package/dist/assets/pieDiagram-8a3498a8-BAUO4fZj.js +0 -35
  47. package/dist/assets/quadrantDiagram-120e2f19-Dxuhsi2y.js +0 -7
  48. package/dist/assets/requirementDiagram-deff3bca-CSBDUaGs.js +0 -52
  49. package/dist/assets/sankeyDiagram-04a897e0-BJP0DjJL.js +0 -8
  50. package/dist/assets/sequenceDiagram-704730f1-BtFLM4FF.js +0 -122
  51. package/dist/assets/stateDiagram-587899a1-iO86zutH.js +0 -1
  52. package/dist/assets/stateDiagram-v2-d93cdb3a-C6nRVeP_.js +0 -1
  53. package/dist/assets/styles-6aaf32cf-NBEyzoLq.js +0 -207
  54. package/dist/assets/styles-9a916d00-BW9zgnwP.js +0 -160
  55. package/dist/assets/styles-c10674c1-CsKPakRe.js +0 -116
  56. package/dist/assets/svgDrawCommon-08f97a94-DdQ1-6ld.js +0 -1
  57. package/dist/assets/timeline-definition-85554ec2-DeLFQELI.js +0 -61
  58. package/dist/assets/workbox-window.prod.es5-D5gOYdM7.js +0 -2
  59. package/dist/assets/xychartDiagram-e933f94c-CiJRsO7D.js +0 -7
  60. package/dist/favicon.ico +0 -0
  61. package/dist/favicon.svg +0 -482
  62. package/dist/index.html +0 -22
  63. package/dist/logo-512x512.png +0 -0
  64. package/dist/logo.svg +0 -482
  65. package/dist/maskable-icon-512x512.png +0 -0
  66. package/dist/pwa-192x192.png +0 -0
  67. package/dist/pwa-512x512.png +0 -0
  68. package/dist/pwa-64x64.png +0 -0
  69. package/dist/sw.js +0 -2
@@ -1,3 +1,4 @@
1
+ const { Agent } = require('https')
1
2
  const path = require('path')
2
3
 
3
4
  const axios = require('axios')
@@ -736,7 +737,7 @@ module.exports = function (RED) {
736
737
  // any widgets we hard-code into our front end (e.g ui-notification for connection alerts) will start with ui-
737
738
  // Node-RED built nodes will be a random UUID
738
739
  if (!wNode && !id.startsWith('ui-')) {
739
- console.log('widget does not exist any more')
740
+ console.log('widget does not exist in the runtime', id) // TODO: Handle this better for edit-time added nodes (e.g. ui-spacer)
740
741
  return // widget does not exist any more (e.g. deleted from NR and deployed BUT the ui page was not refreshed)
741
742
  }
742
743
  async function handler () {
@@ -903,9 +904,9 @@ module.exports = function (RED) {
903
904
  type: widgetConfig.type,
904
905
  props: widgetConfig,
905
906
  layout: {
906
- width: widgetConfig.width || 3,
907
- height: widgetConfig.height || 1,
908
- order: widgetConfig.order || 0
907
+ width: widgetConfig.width || 3, // default width of 3: this must match up with defaults in wysiwyg editing
908
+ height: widgetConfig.height || 1, // default height of 1: this must match up with defaults in wysiwyg editing
909
+ order: widgetConfig.order || 0 // default order of 0: this must match up with defaults in wysiwyg editing
909
910
  },
910
911
  state: statestore.getAll(widgetConfig.id),
911
912
  hooks: widgetEvents,
@@ -1051,12 +1052,13 @@ module.exports = function (RED) {
1051
1052
  } else {
1052
1053
  // msg could be null if the beforeSend errors and returns null
1053
1054
  if (msg) {
1054
- // store the latest msg passed to node
1055
- datastore.save(n, widgetNode, msg)
1056
-
1057
1055
  if (widgetConfig.topic || widgetConfig.topicType) {
1058
1056
  msg = await appendTopic(RED, widgetConfig, wNode, msg)
1059
1057
  }
1058
+
1059
+ // store the latest msg passed to node
1060
+ datastore.save(n, widgetNode, msg)
1061
+
1060
1062
  if (hasProperty(widgetConfig, 'passthru')) {
1061
1063
  if (widgetConfig.passthru) {
1062
1064
  send(msg)
@@ -1144,7 +1146,26 @@ module.exports = function (RED) {
1144
1146
  const host = RED.settings.uiHost
1145
1147
  const port = RED.settings.uiPort
1146
1148
  const httpAdminRoot = RED.settings.httpAdminRoot
1147
- const url = 'http://' + (`${host}:${port}/${httpAdminRoot}flows`).replace('//', '/')
1149
+ let scheme = 'http://'
1150
+ let httpsAgent
1151
+ if (RED.settings.https) {
1152
+ let https = RED.settings.https
1153
+ try {
1154
+ if (typeof https === 'function') {
1155
+ // since https() could return a promise / be async, we need to await it
1156
+ // if however the function is actually sync, JS will auto wrap it in a promise and await it
1157
+ https = await https()
1158
+ }
1159
+ httpsAgent = new Agent({
1160
+ rejectUnauthorized: false,
1161
+ ...(https || {})
1162
+ })
1163
+ scheme = 'https://'
1164
+ } catch (error) {
1165
+ return res.status(500).json({ error: 'Error processing https settings' })
1166
+ }
1167
+ }
1168
+ const url = scheme + (`${host}:${port}/${httpAdminRoot}flows`).replace('//', '/')
1148
1169
  console.log('url', url)
1149
1170
  // get request body
1150
1171
  const dashboardId = req.params.dashboardId
@@ -1152,11 +1173,16 @@ module.exports = function (RED) {
1152
1173
  const changes = req.body.changes || {}
1153
1174
  const editKey = req.body.key
1154
1175
  const groups = changes.groups || []
1176
+ const allWidgets = (changes.widgets || [])
1177
+ const updatedWidgets = allWidgets.filter(w => !w.__DB2_ADD_WIDGET && !w.__DB2_REMOVE_WIDGET)
1178
+ const addedWidgets = allWidgets.filter(w => !!w.__DB2_ADD_WIDGET).map(w => { delete w.__DB2_ADD_WIDGET; return w })
1179
+ const removedWidgets = allWidgets.filter(w => !!w.__DB2_REMOVE_WIDGET).map(w => { delete w.__DB2_REMOVE_WIDGET; return w })
1180
+
1155
1181
  console.log(changes, editKey, dashboardId)
1156
1182
  const baseNode = RED.nodes.getNode(dashboardId)
1157
1183
 
1158
1184
  // validity checks
1159
- if (groups.length === 0) {
1185
+ if (groups.length === 0 && allWidgets.length === 0) {
1160
1186
  // this could be a 200 but since the group data might be missing due to
1161
1187
  // a bug or regression, we'll return a 400 and let the user know
1162
1188
  // there were no changes provided.
@@ -1180,6 +1206,32 @@ module.exports = function (RED) {
1180
1206
  }
1181
1207
  }
1182
1208
 
1209
+ for (const widget of updatedWidgets) {
1210
+ const existingWidget = baseNode.ui.widgets.get(widget.id)
1211
+ if (!existingWidget) {
1212
+ return res.status(400).json({ error: 'Widget not found' })
1213
+ }
1214
+ }
1215
+
1216
+ for (const added of addedWidgets) {
1217
+ // for now, only ui-spacer is supported
1218
+ if (added.type !== 'ui-spacer') {
1219
+ return res.status(400).json({ error: 'Cannot add this kind of widget' })
1220
+ }
1221
+
1222
+ // check if the widget is being added to a valid group
1223
+ const group = baseNode.ui.groups.get(added.group)
1224
+ if (!group) {
1225
+ return res.status(400).json({ error: 'Invalid group id' })
1226
+ }
1227
+ }
1228
+ for (const removed of removedWidgets) {
1229
+ // for now, only ui-spacer is supported
1230
+ if (removed.type !== 'ui-spacer') {
1231
+ return res.status(400).json({ error: 'Cannot remove this kind of widget' })
1232
+ }
1233
+ }
1234
+
1183
1235
  // Prepare headers for the requests
1184
1236
  const getHeaders = {
1185
1237
  'Node-RED-API-Version': 'v2',
@@ -1213,14 +1265,20 @@ module.exports = function (RED) {
1213
1265
  }
1214
1266
  return false
1215
1267
  }
1216
- let rev = null
1217
- return axios.request({
1218
- method: 'GET',
1219
- headers: getHeaders,
1220
- url
1221
- }).then(response => {
1222
- const flows = response.data?.flows || []
1223
- rev = response.data?.rev
1268
+ try {
1269
+ const getResponse = await axios.request({
1270
+ method: 'GET',
1271
+ headers: getHeaders,
1272
+ httpsAgent,
1273
+ url
1274
+ })
1275
+
1276
+ if (getResponse.status !== 200) {
1277
+ return res.status(getResponse.status).json({ error: getResponse?.data?.message || 'An error occurred getting flows', code: 'GET_FAILED' })
1278
+ }
1279
+
1280
+ const flows = getResponse.data?.flows || []
1281
+ const rev = getResponse.data?.rev
1224
1282
  const changeResult = []
1225
1283
  for (const modified of groups) {
1226
1284
  const current = flows.find(n => n.id === modified.id)
@@ -1235,28 +1293,81 @@ module.exports = function (RED) {
1235
1293
  changeResult.push(applyIfDifferent(current, modified, 'width'))
1236
1294
  changeResult.push(applyIfDifferent(current, modified, 'order'))
1237
1295
  }
1296
+ // scan through the widgets and apply changes (if any)
1297
+ for (const modified of updatedWidgets) {
1298
+ const current = flows.find(n => n.id === modified.id)
1299
+ if (!current) {
1300
+ // widget not found in current flows! integrity of data suspect! Has flows changed on the server?
1301
+ return res.status(400).json({ error: 'Widget not found', code: 'WIDGET_NOT_FOUND' })
1302
+ }
1303
+ if (modified.group !== current.group) {
1304
+ // integrity of data suspect! Has flow changed on the server?
1305
+ // Currently we dont support moving widgets between groups
1306
+ return res.status(400).json({ error: 'Invalid group id', code: 'INVALID_GROUP_ID' })
1307
+ }
1308
+ changeResult.push(applyIfDifferent(current, modified, 'order'))
1309
+ changeResult.push(applyIfDifferent(current, modified, 'width'))
1310
+ changeResult.push(applyIfDifferent(current, modified, 'height'))
1311
+ }
1312
+
1313
+ // scan through the added widgets
1314
+ for (const added of addedWidgets) {
1315
+ const current = flows.find(n => n.id === added.id)
1316
+ if (current) {
1317
+ // widget already exists in current flows! integrity of data suspect! Has flows changed on the server?
1318
+ return res.status(400).json({ error: 'Widget already exists', code: 'WIDGET_ALREADY_EXISTS' })
1319
+ }
1320
+ // sanitize the added widget (NOTE: only ui-spacer is supported for now & these are the only properties we care about)
1321
+ const newWidget = {
1322
+ id: added.id,
1323
+ type: added.type,
1324
+ group: added.group,
1325
+ name: added.name || '',
1326
+ order: added.order ?? 0,
1327
+ width: added.width ?? 1,
1328
+ height: added.height ?? 1,
1329
+ className: added.className || ''
1330
+ }
1331
+ flows.push(newWidget)
1332
+ changeResult.push(true)
1333
+ }
1334
+ for (const removed of removedWidgets) {
1335
+ const current = flows.find(n => n.id === removed.id)
1336
+ if (!current) {
1337
+ // widget not found in current flows! integrity of data suspect! Has flows changed on the server?
1338
+ return res.status(400).json({ error: 'Widget not found', code: 'WIDGET_NOT_FOUND' })
1339
+ }
1340
+ const index = flows.indexOf(current)
1341
+ if (index > -1) {
1342
+ flows.splice(index, 1)
1343
+ changeResult.push(true)
1344
+ }
1345
+ }
1238
1346
  if (changeResult.length === 0 || !changeResult.includes(true)) {
1239
- return res.status(200).json({ message: 'No changes were' })
1347
+ return res.status(201).json({ message: 'No changes were found', code: 'NO_CHANGES' })
1240
1348
  }
1241
- return flows
1242
- }).then(flows => {
1243
- // update the flows with the new group order
1244
- return axios.request({
1349
+
1350
+ const postResponse = await axios.request({
1245
1351
  method: 'POST',
1246
1352
  headers: postHeaders,
1353
+ httpsAgent,
1247
1354
  url,
1248
1355
  data: {
1249
1356
  flows,
1250
1357
  rev
1251
1358
  }
1252
1359
  })
1253
- }).then(response => {
1254
- return res.status(200).json(response.data)
1255
- }).catch(error => {
1360
+
1361
+ if (postResponse.status !== 200) {
1362
+ return res.status(postResponse.status).json({ error: postResponse?.data?.message || 'An error occurred deploying flows', code: 'POST_FAILED' })
1363
+ }
1364
+
1365
+ return res.status(postResponse.status).json(postResponse.data)
1366
+ } catch (error) {
1256
1367
  console.error(error)
1257
1368
  const status = error.response?.status || 500
1258
- return res.status(status).json({ error: error.message })
1259
- })
1369
+ return res.status(status).json({ error: error.message || 'An error occurred' })
1370
+ }
1260
1371
  })
1261
1372
 
1262
1373
  // PATCH: /dashboard/api/v1/:dashboardId/edit/:pageId - start editing a page
@@ -38,6 +38,16 @@ function canSaveInStore (base, node, msg) {
38
38
  return checks.length === 0 || !checks.includes(false)
39
39
  }
40
40
 
41
+ // Strip msg of properties that are not needed for storage
42
+ function stripMsg (msg) {
43
+ const newMsg = config.RED.util.cloneMessage(msg)
44
+
45
+ // don't need to store ui_updates in the datastore, as this is handled in statestore
46
+ delete newMsg.ui_update
47
+
48
+ return newMsg
49
+ }
50
+
41
51
  const getters = {
42
52
  RED () {
43
53
  return config.RED
@@ -75,7 +85,11 @@ const setters = {
75
85
  data[node.id] = filtered
76
86
  } else {
77
87
  if (canSaveInStore(base, node, msg)) {
78
- data[node.id] = config.RED.util.cloneMessage(msg)
88
+ const newMsg = stripMsg(msg)
89
+ data[node.id] = {
90
+ ...data[node.id],
91
+ ...newMsg
92
+ }
79
93
  }
80
94
  }
81
95
  },
@@ -0,0 +1,31 @@
1
+ <script type="text/html" data-help-name="ui-audio">
2
+ <p>
3
+ Plays an audio file in the dashboard.
4
+ </p>
5
+ <p>
6
+ Each received <code>msg.payload</code> will contain a new source, i.e. a new audio file url.
7
+ </p>
8
+ <h3>Properties</h3>
9
+ <dl class="message-properties">
10
+ <dt>Source <span class="property-type">string</span></dt>
11
+ <dd>The source is the url where the audio file can be fetched.</dd>
12
+ <dt>Autoplay <span class="property-type">list</span></dt>
13
+ <dd>Specify whether the audio file will start playing automatically.</dd>
14
+ <dt>Loop <span class="property-type">list</span></dt>
15
+ <dd>Specify whether the audio should be looping, i.e. start playing automatically again when ended.</dd>
16
+ <dt>Muted <span class="property-type">list</span></dt>
17
+ <dd>Specify whether the audio should be muted.</dd>
18
+ </dl>
19
+ <h3>Dynamic Properties (Inputs)</h3>
20
+ <p>Any of the following can be appended to a <code>msg.ui_update</code> in order to override or set properties on this node at runtime.</p>
21
+ <dl class="message-properties">
22
+ <dt class="optional">src<span class="property-type">string</span></dt>
23
+ <dd>Override the configured audio source.</dd>
24
+ <dt class="optional">autoplay<span class="property-type">'on' | 'off'</span></dt>
25
+ <dd>Override the configured autoplay setting .</dd>
26
+ <dt class="optional">loop<span class="property-type">'on' | 'off'</span></dt>
27
+ <dd>Override the configured looping behaviour.</dd>
28
+ <dt class="optional">muted<span class="property-type">'on' | 'off'</span></dt>
29
+ <dd>Override the configured muted setting.</dd>
30
+ </dl>
31
+ </script>
@@ -0,0 +1,17 @@
1
+ {
2
+ "ui-audio": {
3
+ "label": {
4
+ "group": "Group",
5
+ "size": "Size",
6
+ "icon": "Icon",
7
+ "source": "Source",
8
+ "autoplay": "Autoplay",
9
+ "loop": "Loop",
10
+ "muted": "Muted"
11
+ },
12
+ "option": {
13
+ "on": "On",
14
+ "off": "Off"
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,113 @@
1
+ <script type="text/javascript">
2
+ (function () {
3
+ RED.nodes.registerType('ui-audio', {
4
+ category: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.label.category'),
5
+ color: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.colors.medium'),
6
+ defaults: {
7
+ group: { type: 'ui-group', required: true },
8
+ name: { value: '' },
9
+ order: { value: 0 },
10
+ width: {
11
+ value: 0,
12
+ validate: function (v) {
13
+ const width = v || 0
14
+ const currentGroup = $('#node-input-group').val() || this.group
15
+ const groupNode = RED.nodes.node(currentGroup)
16
+ const valid = !groupNode || +width >= 0
17
+ $('#node-input-size').toggleClass('input-error', !valid)
18
+ return valid
19
+ }
20
+ },
21
+ height: { value: 0 },
22
+ src: { value: ''},
23
+ autoplay: { value: 'off' },
24
+ loop: { value: 'off' },
25
+ muted: { value: 'off' }
26
+ },
27
+ inputs: 1,
28
+ outputs: 1,
29
+ align: 'right',
30
+ icon: 'font-awesome/fa-volume-up',
31
+ paletteLabel: 'audio',
32
+ label: function () { return this.name },
33
+ labelStyle: function () { return this.name ? 'node_label_italic' : '' },
34
+ oneditprepare: function () {
35
+ // if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up
36
+ // as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
37
+ if (RED.nodes.subflow(this.z)) {
38
+ // change inputs from hidden to text & display them
39
+ $('#node-input-width').attr('type', 'text')
40
+ $('#node-input-height').attr('type', 'text')
41
+ $('div.form-row.nr-db-ui-element-sizer-row').hide()
42
+ $('div.form-row.nr-db-ui-manual-size-row').show()
43
+ } else {
44
+ // not in a subflow, use the elementSizer
45
+ $('div.form-row.nr-db-ui-element-sizer-row').show()
46
+ $('div.form-row.nr-db-ui-manual-size-row').hide()
47
+ $('#node-input-size').elementSizer({
48
+ width: '#node-input-width',
49
+ height: '#node-input-height',
50
+ group: '#node-input-group'
51
+ })
52
+ }
53
+
54
+ // use jQuery UI tooltip to convert the plain old title attribute to a nice tooltip
55
+ $('.ui-node-popover-title').tooltip({
56
+ show: {
57
+ effect: 'slideDown',
58
+ delay: 150
59
+ }
60
+ })
61
+ }
62
+ })
63
+ })()
64
+ </script>
65
+
66
+ <script type="text/html" data-template-name="ui-audio">
67
+ <div class="form-row">
68
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></label>
69
+ <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
70
+ </div>
71
+ <div class="form-row">
72
+ <label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui-audio.label.group"></label>
73
+ <input type="text" id="node-input-group">
74
+ </div>
75
+ <div class="form-row nr-db-ui-element-sizer-row">
76
+ <label><i class="fa fa-object-group"></i> <span data-i18n="ui-audio.label.size"></label>
77
+ <button class="editor-button" id="node-input-size"></button>
78
+ </div>
79
+ <div class="form-row nr-db-ui-manual-size-row">
80
+ <label><i class="fa fa-arrows-h"></i> <span data-i18n="ui-audio.label.width">Width</label>
81
+ <input type="hidden" id="node-input-width">
82
+ </div>
83
+ <div class="form-row nr-db-ui-manual-size-row">
84
+ <label><i class="fa fa-arrows-v"></i> <span data-i18n="ui-audio.label.height">Height</label>
85
+ <input type="hidden" id="node-input-height">
86
+ </div>
87
+ <div class="form-row">
88
+ <label for="node-input-src"><i class="fa fa-globe"></i> <span data-i18n="ui-audio.label.source"></label>
89
+ <input type="text" id="node-input-src">
90
+ </div>
91
+ <div class="form-row">
92
+ <label for="node-input-autoplay"><i class="fa fa-play-circle"></i> <span data-i18n="ui-audio.label.autoplay"></label>
93
+ <select id="node-input-autoplay" style="width:70%;">
94
+ <option value="on" data-i18n="ui-audio.option.on"></option>
95
+ <option value="off" data-i18n="ui-audio.option.off"></option>
96
+ </select>
97
+ </div>
98
+ <div class="form-row">
99
+ <label for="node-input-loop"><i class="fa fa-retweet"></i> <span data-i18n="ui-audio.label.loop"></label>
100
+ <select id="node-input-loop" style="width:70%;">
101
+ <option value="on" data-i18n="ui-audio.option.on"></option>
102
+ <option value="off" data-i18n="ui-audio.option.off"></option>
103
+ </select>
104
+ </div>
105
+ <div class="form-row">
106
+ <label for="node-input-muted"><i class="fa fa-volume-up"></i> <span data-i18n="ui-audio.label.muted"></label>
107
+ <select id="node-input-muted" style="width:70%;">
108
+ <option value="on" data-i18n="ui-audio.option.on"></option>
109
+ <option value="off" data-i18n="ui-audio.option.off"></option>
110
+ </select>
111
+ </div>
112
+ <div class="form-tips"><b>Note</b>: Autoplay will only work after a user gesture (e.g. click on the dashboard).</span></div>
113
+ </script>
@@ -0,0 +1,81 @@
1
+ const datastore = require('../store/data.js')
2
+ const statestore = require('../store/state.js')
3
+
4
+ module.exports = function (RED) {
5
+ function AudioNode (config) {
6
+ const node = this
7
+
8
+ RED.nodes.createNode(this, config)
9
+
10
+ // which group are we rendering this widget
11
+ const group = RED.nodes.getNode(config.group)
12
+
13
+ const evts = {
14
+ onAction: true,
15
+ onInput: function (msg, send) {
16
+ // store the latest msg passed to node, only if a source is supplied in the payload
17
+ if (typeof msg.payload === 'string') {
18
+ datastore.save(group.getBase(), node, msg)
19
+ }
20
+ // only send msg on if we have passthru enabled
21
+ if (config.passthru) {
22
+ send(msg)
23
+ }
24
+ },
25
+ beforeSend: function (msg) {
26
+ if (msg.playback === 'play') {
27
+ const lastMsg = datastore.get(node.id)
28
+ // TODO zou eigenlijk de last message met een payload moeten zijn.
29
+ const src = lastMsg?.payload || config.src
30
+ if (typeof src !== 'string' || src.trim() === '') {
31
+ node.warn('Cannot play audio when no source has been specified')
32
+ }
33
+ }
34
+
35
+ if (msg.ui_update) {
36
+ const updates = msg.ui_update
37
+
38
+ if (updates) {
39
+ if (typeof updates.src !== 'undefined') {
40
+ // dynamically set "src" property
41
+ statestore.set(group.getBase(), node, msg, 'src', updates.src)
42
+ }
43
+ if (typeof updates.autoplay !== 'undefined') {
44
+ if (['on', 'off'].includes(updates.autoplay)) {
45
+ // dynamically set "autoplay" property
46
+ statestore.set(group.getBase(), node, msg, 'autoplay', updates.autoplay)
47
+ } else {
48
+ node.error('Property msg.ui_update.autoplay should be "on" or "off"')
49
+ }
50
+ }
51
+ if (typeof updates.loop !== 'undefined') {
52
+ if (['on', 'off'].includes(updates.loop)) {
53
+ // dynamically set "loop" property
54
+ statestore.set(group.getBase(), node, msg, 'loop', updates.loop)
55
+ } else {
56
+ node.error('Property msg.ui_update.loop should be "on" or "off"')
57
+ }
58
+ }
59
+ if (typeof updates.muted !== 'undefined') {
60
+ if (['on', 'off'].includes(updates.muted)) {
61
+ // dynamically set "muted" property
62
+ statestore.set(group.getBase(), node, msg, 'muted', updates.muted)
63
+ } else {
64
+ node.error('Property msg.ui_update.muted should be "on" or "off"')
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return msg
70
+ }
71
+ }
72
+
73
+ // inform the dashboard UI that we are adding this node
74
+ if (group) {
75
+ group.register(node, config, evts)
76
+ } else {
77
+ node.error('No group configured')
78
+ }
79
+ }
80
+ RED.nodes.registerType('ui-audio', AudioNode)
81
+ }
@@ -19,7 +19,7 @@ module.exports = function (RED) {
19
19
 
20
20
  // get max file size supported
21
21
  const MAX_FILESIZE_DEFAULT = 1e6
22
- const maxFileSize = RED.settings.dashboard?.maxHttpBufferSize || MAX_FILESIZE_DEFAULT
22
+ const maxFileSize = RED.settings.dashboard?.maxHttpBufferSize || RED.settings.ui?.maxHttpBufferSize || MAX_FILESIZE_DEFAULT
23
23
 
24
24
  config.maxFileSize = maxFileSize
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowfuse/node-red-dashboard",
3
- "version": "1.20.1",
3
+ "version": "1.20.2-357ebba-202412181736.0",
4
4
  "description": "Dashboard 2.0 - A collection of Node-RED nodes that provide functionality to build your own UI applications (inc. forms, buttons, charts).",
5
5
  "keywords": [
6
6
  "node-red"
@@ -143,6 +143,7 @@
143
143
  "ui-chart": "nodes/widgets/ui_chart.js",
144
144
  "ui-gauge": "nodes/widgets/ui_gauge.js",
145
145
  "ui-notification": "nodes/widgets/ui_notification.js",
146
+ "ui-audio": "nodes/widgets/ui_audio.js",
146
147
  "ui-markdown": "nodes/widgets/ui_markdown.js",
147
148
  "ui-template": "nodes/widgets/ui_template.js",
148
149
  "ui-event": "nodes/widgets/ui_event.js",
Binary file
@@ -1 +0,0 @@
1
- function o(e){for(var c=e.length/6|0,n=new Array(c),a=0;a<c;)n[a]="#"+e.slice(a*6,++a*6);return n}const r=o("4e79a7f28e2ce1575976b7b259a14fedc949af7aa1ff9da79c755fbab0ab");export{r as s};
@@ -1 +0,0 @@
1
- function t(r){return typeof r=="object"&&"length"in r?r:Array.from(r)}export{t as a};