Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repurpose the datastores to merge values, not replace #1465

Merged
merged 5 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -1052,12 +1052,13 @@ module.exports = function (RED) {
} else {
// msg could be null if the beforeSend errors and returns null
if (msg) {
// store the latest msg passed to node
datastore.save(n, widgetNode, msg)

if (widgetConfig.topic || widgetConfig.topicType) {
msg = await appendTopic(RED, widgetConfig, wNode, msg)
}

// store the latest msg passed to node
datastore.save(n, widgetNode, msg)

if (hasProperty(widgetConfig, 'passthru')) {
if (widgetConfig.passthru) {
send(msg)
Expand Down
16 changes: 15 additions & 1 deletion nodes/store/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ function canSaveInStore (base, node, msg) {
return checks.length === 0 || !checks.includes(false)
}

// Strip msg of properties that are not needed for storage
function stripMsg (msg) {
const newMsg = config.RED.util.cloneMessage(msg)

// don't need to store ui_updates in the datastore, as this is handled in statestore
delete newMsg.ui_update

return newMsg
}

const getters = {
RED () {
return config.RED
Expand Down Expand Up @@ -75,7 +85,11 @@ const setters = {
data[node.id] = filtered
} else {
if (canSaveInStore(base, node, msg)) {
data[node.id] = config.RED.util.cloneMessage(msg)
const newMsg = stripMsg(msg)
data[node.id] = {
...data[node.id],
...newMsg
}
}
}
},
Expand Down
23 changes: 5 additions & 18 deletions ui/src/store/data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Vuex store for tracking data bound to each widget
*/

import { getDeepValue, hasProperty } from '../util.mjs'
import { getDeepValue } from '../util.mjs'

// initial state is empty - we don't know if we have any widgets
const state = () => ({
Expand All @@ -11,29 +11,16 @@ const state = () => ({
properties: {}
})

// map of supported property messages
// Any msg received with a topic matching a key in this object will be stored in the properties object under the value of the key
// e.g. { topic: 'ui-property:class', payload: 'my-class' } will be stored as { class: 'my-class' }
const supportedPropertyMessages = {
'ui-property:class': 'class'
}

const mutations = {
bind (state, data) {
const widgetId = data.widgetId
// if packet contains a msg, then we process it
if ('msg' in data) {
// first, if the msg.topic is a supported property message, then we store it in the properties object
// but do not store it in the messages object.
// This permits the widget to receive property messages without affecting the widget's value
if (data.msg?.topic && supportedPropertyMessages[data.msg.topic] && hasProperty(data.msg, 'payload')) {
const controlProperty = supportedPropertyMessages[data.msg.topic]
state.properties[widgetId] = state.properties[widgetId] || {}
state.properties[widgetId][controlProperty] = data.msg.payload
return // do not store in messages object
// merge with any existing data and override relevant properties
state.messages[widgetId] = {
...state.messages[widgetId],
...data.msg
}
// if the msg was not a property message, then we store it in the messages object
state.messages[widgetId] = data.msg
}
},
append (state, data) {
Expand Down
67 changes: 21 additions & 46 deletions ui/src/widgets/ui-button-group/UIButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ export default {
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
data () {
return {
selection: null
}
},
computed: {
...mapState('data', ['messages']),
selectedColor: function () {
Expand Down Expand Up @@ -59,63 +54,43 @@ export default {
})
}
return options
},
selection: {
get () {
const msg = this.messages[this.id]
let selection = null
if (msg) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
selection = null
} else if (this.findOptionByValue(msg.payload) !== null) {
selection = msg.payload
}
}
return selection
},
set (value) {
if (!this.messages[this.id]) {
this.messages[this.id] = {}
}
this.messages[this.id].payload = value
}
}
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperty, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperty, null)

// let Node-RED know that this widget has loaded
this.$socket.emit('widget-load', this.id)
},
methods: {
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})

// make sure our v-model is updated to reflect the value from Node-RED
if (msg.payload !== undefined) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
this.selection = null
} else {
if (this.findOptionByValue(msg.payload) !== null) {
this.selection = msg.payload
}
}
}
},
onLoad (msg) {
if (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg.payload !== undefined) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
this.selection = null
} else {
if (this.findOptionByValue(msg.payload) !== null) {
this.selection = msg.payload
}
}
}
}
},
onDynamicProperty (msg) {
const updates = msg.ui_update
if (updates) {
this.updateDynamicProperty('label', updates.label)
this.updateDynamicProperty('options', updates.options)
}
},
onSync (msg) {
this.selection = msg.payload
},
onChange (value) {
if (value !== null && typeof value !== 'undefined') {
// Tell Node-RED a new value has been selected
Expand Down
52 changes: 6 additions & 46 deletions ui/src/widgets/ui-number-input/UINumberInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ export default {
data () {
return {
delayTimer: null,
textValue: null,
previousValue: null,
isCompressed: false
}
},
Expand Down Expand Up @@ -109,18 +107,18 @@ export default {
},
value: {
get () {
if (this.textValue === null || this.textValue === undefined || this.textValue === '') {
return this.textValue
const val = this.messages[this.id]?.payload
if (val === null || val === undefined || val === '') {
return val
} else {
return Number(this.textValue)
return Number(val)
}
},
set (val) {
if (this.value === val) {
return // no change
}
const msg = this.messages[this.id] || {}
this.textValue = val
msg.payload = val
this.messages[this.id] = msg
}
Expand Down Expand Up @@ -170,52 +168,14 @@ export default {
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperties, null)
},
methods: {
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure our v-model is updated to reflect the value from Node-RED
if (msg.payload !== undefined) {
this.textValue = msg.payload
}
},
onLoad (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg?.payload !== undefined) {
this.textValue = msg.payload
this.previousValue = msg.payload
}
},
onSync (msg) {
if (typeof (msg?.payload) !== 'undefined') {
this.textValue = msg.payload
this.previousValue = msg.payload
}
},
send () {
this.$socket.emit('widget-change', this.id, this.value)
},
onChange () {
// Since the Vuetify Input Number component doesn't currently support an onClick event,
// compare the previous value with the current value and check whether the value has been increased or decreased by one.
if (
this.previousValue === null ||
this.previousValue + (this.step || 1) === this.value ||
this.previousValue - (this.step || 1) === this.value
) {
this.send()
}
this.previousValue = this.value
this.send()
},
onBlur: function () {
if (this.props.sendOnBlur) {
Expand Down
7 changes: 1 addition & 6 deletions ui/src/widgets/ui-slider/UISlider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default {
}
},
created () {
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperties)
},
mounted () {
const val = this.messages[this.id]?.payload
Expand Down Expand Up @@ -196,11 +196,6 @@ export default {
this.updateDynamicProperty('colorThumb', updates.colorThumb)
this.updateDynamicProperty('showTextField', updates.showTextField)
},
onSync (msg) {
if (typeof msg?.payload !== 'undefined') {
this.sliderValue = Number(msg.payload)
}
},
// Validate the text field input
validateInput () {
this.textFieldValue = this.roundToStep(this.textFieldValue)
Expand Down
59 changes: 13 additions & 46 deletions ui/src/widgets/ui-text-input/UITextInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,22 @@ export default {
},
data () {
return {
delayTimer: null,
textValue: null
delayTimer: null
}
},
computed: {
...mapState('data', ['messages']),
value: {
get () {
return this.messages[this.id]?.payload
},
set (val) {
if (!this.messages[this.id]) {
this.messages[this.id] = {}
}
this.messages[this.id].payload = val
}
},
label: function () {
// Sanetize the html to avoid XSS attacks
return DOMPurify.sanitize(this.getProperty('label'))
Expand Down Expand Up @@ -103,20 +113,6 @@ export default {
iconInnerPosition () {
return this.getProperty('iconInnerPosition')
},
value: {
get () {
return this.textValue
},
set (val) {
if (this.value === val) {
return // no change
}
const msg = this.messages[this.id] || {}
this.textValue = val
msg.payload = val
this.messages[this.id] = msg
}
},
validation: function () {
if (this.type === 'email') {
return [v => !v || /^[^\s@]+@[^\s@]+$/.test(v) || 'E-mail must be valid']
Expand All @@ -127,38 +123,9 @@ export default {
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperties)
},
methods: {
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure our v-model is updated to reflect the value from Node-RED
if (msg.payload !== undefined) {
this.textValue = msg.payload
}
},
onLoad (msg) {
if (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg.payload !== undefined) {
this.textValue = msg.payload
}
}
},
onSync (msg) {
if (typeof (msg.payload) !== 'undefined') {
this.textValue = msg.payload
}
},
send: function () {
this.$socket.emit('widget-change', this.id, this.value)
},
Expand Down
Loading
Loading