diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index 3bfd41bfd..97dd6f4e9 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -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) diff --git a/nodes/store/data.js b/nodes/store/data.js index 32810d148..47693c0af 100644 --- a/nodes/store/data.js +++ b/nodes/store/data.js @@ -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 @@ -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 + } } } }, diff --git a/ui/src/store/data.mjs b/ui/src/store/data.mjs index 459d00224..96f3554d7 100644 --- a/ui/src/store/data.mjs +++ b/ui/src/store/data.mjs @@ -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 = () => ({ @@ -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) { diff --git a/ui/src/widgets/ui-button-group/UIButtonGroup.vue b/ui/src/widgets/ui-button-group/UIButtonGroup.vue index 86cf4882d..4cfc2ff65 100644 --- a/ui/src/widgets/ui-button-group/UIButtonGroup.vue +++ b/ui/src/widgets/ui-button-group/UIButtonGroup.vue @@ -26,11 +26,6 @@ export default { props: { type: Object, default: () => ({}) }, state: { type: Object, default: () => ({}) } }, - data () { - return { - selection: null - } - }, computed: { ...mapState('data', ['messages']), selectedColor: function () { @@ -59,53 +54,36 @@ 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) { @@ -113,9 +91,6 @@ export default { 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 diff --git a/ui/src/widgets/ui-number-input/UINumberInput.vue b/ui/src/widgets/ui-number-input/UINumberInput.vue index 61f9a79e3..9c3c2c50f 100644 --- a/ui/src/widgets/ui-number-input/UINumberInput.vue +++ b/ui/src/widgets/ui-number-input/UINumberInput.vue @@ -40,8 +40,6 @@ export default { data () { return { delayTimer: null, - textValue: null, - previousValue: null, isCompressed: false } }, @@ -109,10 +107,11 @@ 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) { @@ -120,7 +119,6 @@ export default { return // no change } const msg = this.messages[this.id] || {} - this.textValue = val msg.payload = val this.messages[this.id] = msg } @@ -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) { diff --git a/ui/src/widgets/ui-slider/UISlider.vue b/ui/src/widgets/ui-slider/UISlider.vue index 0cbb1ebb0..a1a872142 100644 --- a/ui/src/widgets/ui-slider/UISlider.vue +++ b/ui/src/widgets/ui-slider/UISlider.vue @@ -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 @@ -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) diff --git a/ui/src/widgets/ui-text-input/UITextInput.vue b/ui/src/widgets/ui-text-input/UITextInput.vue index 41aa29aee..3a391d3b0 100644 --- a/ui/src/widgets/ui-text-input/UITextInput.vue +++ b/ui/src/widgets/ui-text-input/UITextInput.vue @@ -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')) @@ -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'] @@ -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) }, diff --git a/ui/src/widgets/ui-text/UIText.vue b/ui/src/widgets/ui-text/UIText.vue index 75d20baeb..731dab4c1 100644 --- a/ui/src/widgets/ui-text/UIText.vue +++ b/ui/src/widgets/ui-text/UIText.vue @@ -26,8 +26,9 @@ export default { }, computed: { ...mapState('data', ['messages', 'properties']), - value: function () { - return this.textValue + value () { + const msg = this.messages[this.id] + return this.purify(msg?.payload) }, label () { // Sanitize the html to avoid XSS attacks @@ -51,7 +52,7 @@ export default { } }, created () { - this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties) + this.$dataTracker(this.id, null, null, this.onDynamicProperties) }, methods: { onDynamicProperties (msg) { @@ -65,31 +66,6 @@ export default { this.updateDynamicProperty('fontSize', updates.fontSize) this.updateDynamicProperty('color', updates.color) }, - 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 (Object.prototype.hasOwnProperty.call(msg, 'payload')) { - // Sanitize the HTML to avoid XSS attacks - this.textValue = this.purify(msg.payload) - } - }, - onLoad (msg) { - if (msg) { - // update vuex store to reflect server-state - this.$store.commit('data/bind', { - widgetId: this.id, - msg - }) - if (Object.prototype.hasOwnProperty.call(msg, 'payload')) { - // Sanitize the HTML to avoid XSS attacks - this.textValue = this.purify(msg.payload) - } - } - }, purify (payload) { if (typeof payload === 'string') { return DOMPurify.sanitize(payload, { ADD_ATTR: ['target'] })