@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.
- package/nodes/config/ui_base.js +138 -27
- package/nodes/store/data.js +15 -1
- package/nodes/widgets/locales/en-US/ui_audio.html +31 -0
- package/nodes/widgets/locales/en-US/ui_audio.json +17 -0
- package/nodes/widgets/ui_audio.html +113 -0
- package/nodes/widgets/ui_audio.js +81 -0
- package/nodes/widgets/ui_file_input.js +1 -1
- package/package.json +2 -1
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Tableau10-B-NsZVaP.js +0 -1
- package/dist/assets/array-BKyUJesY.js +0 -1
- package/dist/assets/blockDiagram-38ab4fdb-CMxXCB2U.js +0 -118
- package/dist/assets/c4Diagram-3d4e48cf-Cp5hSJnl.js +0 -10
- package/dist/assets/channel-DNOEREZ5.js +0 -1
- package/dist/assets/classDiagram-70f12bd4-B3RxdJz8.js +0 -2
- package/dist/assets/classDiagram-v2-f2320105-8HvDjcCx.js +0 -2
- package/dist/assets/clone-DUx8NCf1.js +0 -1
- package/dist/assets/createText-2e5e7dd3-Dd_JTvu4.js +0 -7
- package/dist/assets/disconnected-BuxohaUu.png +0 -0
- package/dist/assets/edges-e0da2a9e-DOHGlYkB.js +0 -4
- package/dist/assets/erDiagram-9861fffd-yOdzCO98.js +0 -51
- package/dist/assets/flowDb-956e92f1-CBOVQW-G.js +0 -10
- package/dist/assets/flowDiagram-66a62f08-DUeZEotM.js +0 -4
- package/dist/assets/flowDiagram-v2-96b9c2cf-dkHS5k1P.js +0 -1
- package/dist/assets/flowchart-elk-definition-4a651766-DiHGz-_a.js +0 -139
- package/dist/assets/ganttDiagram-c361ad54-DVPcSn9h.js +0 -257
- package/dist/assets/gitGraphDiagram-72cf32ee-BxXHXf9m.js +0 -70
- package/dist/assets/graph-D1W14WGM.js +0 -1
- package/dist/assets/index-3862675e-Czbct74y.js +0 -1
- package/dist/assets/index-BbWwSxsY.js +0 -250
- package/dist/assets/index-MyTOKb8y.css +0 -13
- package/dist/assets/infoDiagram-f8f76790-Dx3Iio-0.js +0 -7
- package/dist/assets/init-Gi6I4Gst.js +0 -1
- package/dist/assets/journeyDiagram-49397b02-wHSLlApM.js +0 -139
- package/dist/assets/katex-CvgdMzdh.js +0 -261
- package/dist/assets/layout-BpRCMw1H.js +0 -1
- package/dist/assets/line-DDza7Wzt.js +0 -1
- package/dist/assets/linear-CJJq30CP.js +0 -1
- package/dist/assets/logo-DIAzbBuw.png +0 -0
- package/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/assets/mindmap-definition-fc14e90a-BIT01dqm.js +0 -110
- package/dist/assets/ordinal-Cboi1Yqb.js +0 -1
- package/dist/assets/pieDiagram-8a3498a8-BAUO4fZj.js +0 -35
- package/dist/assets/quadrantDiagram-120e2f19-Dxuhsi2y.js +0 -7
- package/dist/assets/requirementDiagram-deff3bca-CSBDUaGs.js +0 -52
- package/dist/assets/sankeyDiagram-04a897e0-BJP0DjJL.js +0 -8
- package/dist/assets/sequenceDiagram-704730f1-BtFLM4FF.js +0 -122
- package/dist/assets/stateDiagram-587899a1-iO86zutH.js +0 -1
- package/dist/assets/stateDiagram-v2-d93cdb3a-C6nRVeP_.js +0 -1
- package/dist/assets/styles-6aaf32cf-NBEyzoLq.js +0 -207
- package/dist/assets/styles-9a916d00-BW9zgnwP.js +0 -160
- package/dist/assets/styles-c10674c1-CsKPakRe.js +0 -116
- package/dist/assets/svgDrawCommon-08f97a94-DdQ1-6ld.js +0 -1
- package/dist/assets/timeline-definition-85554ec2-DeLFQELI.js +0 -61
- package/dist/assets/workbox-window.prod.es5-D5gOYdM7.js +0 -2
- package/dist/assets/xychartDiagram-e933f94c-CiJRsO7D.js +0 -7
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +0 -482
- package/dist/index.html +0 -22
- package/dist/logo-512x512.png +0 -0
- package/dist/logo.svg +0 -482
- package/dist/maskable-icon-512x512.png +0 -0
- package/dist/pwa-192x192.png +0 -0
- package/dist/pwa-512x512.png +0 -0
- package/dist/pwa-64x64.png +0 -0
- package/dist/sw.js +0 -2
package/nodes/config/ui_base.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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(
|
|
1347
|
+
return res.status(201).json({ message: 'No changes were found', code: 'NO_CHANGES' })
|
|
1240
1348
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
package/nodes/store/data.js
CHANGED
|
@@ -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
|
-
|
|
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,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.
|
|
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};
|