Skip to content

Commit 357ebba

Browse files
authored
Merge pull request #1465 from FlowFuse/1294-data-storage
Repurpose the datastores to merge values, not replace
2 parents c9e45ff + fa255c0 commit 357ebba

File tree

8 files changed

+69
-194
lines changed

8 files changed

+69
-194
lines changed

nodes/config/ui_base.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,12 +1052,13 @@ module.exports = function (RED) {
10521052
} else {
10531053
// msg could be null if the beforeSend errors and returns null
10541054
if (msg) {
1055-
// store the latest msg passed to node
1056-
datastore.save(n, widgetNode, msg)
1057-
10581055
if (widgetConfig.topic || widgetConfig.topicType) {
10591056
msg = await appendTopic(RED, widgetConfig, wNode, msg)
10601057
}
1058+
1059+
// store the latest msg passed to node
1060+
datastore.save(n, widgetNode, msg)
1061+
10611062
if (hasProperty(widgetConfig, 'passthru')) {
10621063
if (widgetConfig.passthru) {
10631064
send(msg)

nodes/store/data.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ function canSaveInStore (base, node, msg) {
3838
return checks.length === 0 || !checks.includes(false)
3939
}
4040

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+
4151
const getters = {
4252
RED () {
4353
return config.RED
@@ -75,7 +85,11 @@ const setters = {
7585
data[node.id] = filtered
7686
} else {
7787
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+
}
7993
}
8094
}
8195
},

ui/src/store/data.mjs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Vuex store for tracking data bound to each widget
33
*/
44

5-
import { getDeepValue, hasProperty } from '../util.mjs'
5+
import { getDeepValue } from '../util.mjs'
66

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

14-
// map of supported property messages
15-
// 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
16-
// e.g. { topic: 'ui-property:class', payload: 'my-class' } will be stored as { class: 'my-class' }
17-
const supportedPropertyMessages = {
18-
'ui-property:class': 'class'
19-
}
20-
2114
const mutations = {
2215
bind (state, data) {
2316
const widgetId = data.widgetId
2417
// if packet contains a msg, then we process it
2518
if ('msg' in data) {
26-
// first, if the msg.topic is a supported property message, then we store it in the properties object
27-
// but do not store it in the messages object.
28-
// This permits the widget to receive property messages without affecting the widget's value
29-
if (data.msg?.topic && supportedPropertyMessages[data.msg.topic] && hasProperty(data.msg, 'payload')) {
30-
const controlProperty = supportedPropertyMessages[data.msg.topic]
31-
state.properties[widgetId] = state.properties[widgetId] || {}
32-
state.properties[widgetId][controlProperty] = data.msg.payload
33-
return // do not store in messages object
19+
// merge with any existing data and override relevant properties
20+
state.messages[widgetId] = {
21+
...state.messages[widgetId],
22+
...data.msg
3423
}
35-
// if the msg was not a property message, then we store it in the messages object
36-
state.messages[widgetId] = data.msg
3724
}
3825
},
3926
append (state, data) {

ui/src/widgets/ui-button-group/UIButtonGroup.vue

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ export default {
2626
props: { type: Object, default: () => ({}) },
2727
state: { type: Object, default: () => ({}) }
2828
},
29-
data () {
30-
return {
31-
selection: null
32-
}
33-
},
3429
computed: {
3530
...mapState('data', ['messages']),
3631
selectedColor: function () {
@@ -59,63 +54,43 @@ export default {
5954
})
6055
}
6156
return options
57+
},
58+
selection: {
59+
get () {
60+
const msg = this.messages[this.id]
61+
let selection = null
62+
if (msg) {
63+
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
64+
selection = null
65+
} else if (this.findOptionByValue(msg.payload) !== null) {
66+
selection = msg.payload
67+
}
68+
}
69+
return selection
70+
},
71+
set (value) {
72+
if (!this.messages[this.id]) {
73+
this.messages[this.id] = {}
74+
}
75+
this.messages[this.id].payload = value
76+
}
6277
}
6378
},
6479
created () {
6580
// can't do this in setup as we are using custom onInput function that needs access to 'this'
66-
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperty, this.onSync)
81+
this.$dataTracker(this.id, null, null, this.onDynamicProperty, null)
6782
6883
// let Node-RED know that this widget has loaded
6984
this.$socket.emit('widget-load', this.id)
7085
},
7186
methods: {
72-
onInput (msg) {
73-
// update our vuex store with the value retrieved from Node-RED
74-
this.$store.commit('data/bind', {
75-
widgetId: this.id,
76-
msg
77-
})
78-
79-
// make sure our v-model is updated to reflect the value from Node-RED
80-
if (msg.payload !== undefined) {
81-
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
82-
this.selection = null
83-
} else {
84-
if (this.findOptionByValue(msg.payload) !== null) {
85-
this.selection = msg.payload
86-
}
87-
}
88-
}
89-
},
90-
onLoad (msg) {
91-
if (msg) {
92-
// update vuex store to reflect server-state
93-
this.$store.commit('data/bind', {
94-
widgetId: this.id,
95-
msg
96-
})
97-
// make sure we've got the relevant option selected on load of the page
98-
if (msg.payload !== undefined) {
99-
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
100-
this.selection = null
101-
} else {
102-
if (this.findOptionByValue(msg.payload) !== null) {
103-
this.selection = msg.payload
104-
}
105-
}
106-
}
107-
}
108-
},
10987
onDynamicProperty (msg) {
11088
const updates = msg.ui_update
11189
if (updates) {
11290
this.updateDynamicProperty('label', updates.label)
11391
this.updateDynamicProperty('options', updates.options)
11492
}
11593
},
116-
onSync (msg) {
117-
this.selection = msg.payload
118-
},
11994
onChange (value) {
12095
if (value !== null && typeof value !== 'undefined') {
12196
// Tell Node-RED a new value has been selected

ui/src/widgets/ui-number-input/UINumberInput.vue

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ export default {
4040
data () {
4141
return {
4242
delayTimer: null,
43-
textValue: null,
44-
previousValue: null,
4543
isCompressed: false
4644
}
4745
},
@@ -109,18 +107,18 @@ export default {
109107
},
110108
value: {
111109
get () {
112-
if (this.textValue === null || this.textValue === undefined || this.textValue === '') {
113-
return this.textValue
110+
const val = this.messages[this.id]?.payload
111+
if (val === null || val === undefined || val === '') {
112+
return val
114113
} else {
115-
return Number(this.textValue)
114+
return Number(val)
116115
}
117116
},
118117
set (val) {
119118
if (this.value === val) {
120119
return // no change
121120
}
122121
const msg = this.messages[this.id] || {}
123-
this.textValue = val
124122
msg.payload = val
125123
this.messages[this.id] = msg
126124
}
@@ -170,52 +168,14 @@ export default {
170168
},
171169
created () {
172170
// can't do this in setup as we are using custom onInput function that needs access to 'this'
173-
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
171+
this.$dataTracker(this.id, null, null, this.onDynamicProperties, null)
174172
},
175173
methods: {
176-
onInput (msg) {
177-
// update our vuex store with the value retrieved from Node-RED
178-
this.$store.commit('data/bind', {
179-
widgetId: this.id,
180-
msg
181-
})
182-
// make sure our v-model is updated to reflect the value from Node-RED
183-
if (msg.payload !== undefined) {
184-
this.textValue = msg.payload
185-
}
186-
},
187-
onLoad (msg) {
188-
// update vuex store to reflect server-state
189-
this.$store.commit('data/bind', {
190-
widgetId: this.id,
191-
msg
192-
})
193-
// make sure we've got the relevant option selected on load of the page
194-
if (msg?.payload !== undefined) {
195-
this.textValue = msg.payload
196-
this.previousValue = msg.payload
197-
}
198-
},
199-
onSync (msg) {
200-
if (typeof (msg?.payload) !== 'undefined') {
201-
this.textValue = msg.payload
202-
this.previousValue = msg.payload
203-
}
204-
},
205174
send () {
206175
this.$socket.emit('widget-change', this.id, this.value)
207176
},
208177
onChange () {
209-
// Since the Vuetify Input Number component doesn't currently support an onClick event,
210-
// compare the previous value with the current value and check whether the value has been increased or decreased by one.
211-
if (
212-
this.previousValue === null ||
213-
this.previousValue + (this.step || 1) === this.value ||
214-
this.previousValue - (this.step || 1) === this.value
215-
) {
216-
this.send()
217-
}
218-
this.previousValue = this.value
178+
this.send()
219179
},
220180
onBlur: function () {
221181
if (this.props.sendOnBlur) {

ui/src/widgets/ui-slider/UISlider.vue

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export default {
132132
}
133133
},
134134
created () {
135-
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties, this.onSync)
135+
this.$dataTracker(this.id, null, null, this.onDynamicProperties)
136136
},
137137
mounted () {
138138
const val = this.messages[this.id]?.payload
@@ -196,11 +196,6 @@ export default {
196196
this.updateDynamicProperty('colorThumb', updates.colorThumb)
197197
this.updateDynamicProperty('showTextField', updates.showTextField)
198198
},
199-
onSync (msg) {
200-
if (typeof msg?.payload !== 'undefined') {
201-
this.sliderValue = Number(msg.payload)
202-
}
203-
},
204199
// Validate the text field input
205200
validateInput () {
206201
this.textFieldValue = this.roundToStep(this.textFieldValue)

ui/src/widgets/ui-text-input/UITextInput.vue

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,22 @@ export default {
4646
},
4747
data () {
4848
return {
49-
delayTimer: null,
50-
textValue: null
49+
delayTimer: null
5150
}
5251
},
5352
computed: {
5453
...mapState('data', ['messages']),
54+
value: {
55+
get () {
56+
return this.messages[this.id]?.payload
57+
},
58+
set (val) {
59+
if (!this.messages[this.id]) {
60+
this.messages[this.id] = {}
61+
}
62+
this.messages[this.id].payload = val
63+
}
64+
},
5565
label: function () {
5666
// Sanetize the html to avoid XSS attacks
5767
return DOMPurify.sanitize(this.getProperty('label'))
@@ -103,20 +113,6 @@ export default {
103113
iconInnerPosition () {
104114
return this.getProperty('iconInnerPosition')
105115
},
106-
value: {
107-
get () {
108-
return this.textValue
109-
},
110-
set (val) {
111-
if (this.value === val) {
112-
return // no change
113-
}
114-
const msg = this.messages[this.id] || {}
115-
this.textValue = val
116-
msg.payload = val
117-
this.messages[this.id] = msg
118-
}
119-
},
120116
validation: function () {
121117
if (this.type === 'email') {
122118
return [v => !v || /^[^\s@]+@[^\s@]+$/.test(v) || 'E-mail must be valid']
@@ -127,38 +123,9 @@ export default {
127123
},
128124
created () {
129125
// can't do this in setup as we are using custom onInput function that needs access to 'this'
130-
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
126+
this.$dataTracker(this.id, null, null, this.onDynamicProperties)
131127
},
132128
methods: {
133-
onInput (msg) {
134-
// update our vuex store with the value retrieved from Node-RED
135-
this.$store.commit('data/bind', {
136-
widgetId: this.id,
137-
msg
138-
})
139-
// make sure our v-model is updated to reflect the value from Node-RED
140-
if (msg.payload !== undefined) {
141-
this.textValue = msg.payload
142-
}
143-
},
144-
onLoad (msg) {
145-
if (msg) {
146-
// update vuex store to reflect server-state
147-
this.$store.commit('data/bind', {
148-
widgetId: this.id,
149-
msg
150-
})
151-
// make sure we've got the relevant option selected on load of the page
152-
if (msg.payload !== undefined) {
153-
this.textValue = msg.payload
154-
}
155-
}
156-
},
157-
onSync (msg) {
158-
if (typeof (msg.payload) !== 'undefined') {
159-
this.textValue = msg.payload
160-
}
161-
},
162129
send: function () {
163130
this.$socket.emit('widget-change', this.id, this.value)
164131
},

0 commit comments

Comments
 (0)