node-red-contrib-room-tools 1.4.2 → 1.7.1
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/CONTROL_PANEL_AI_GUIDE.md +220 -0
- package/README.md +85 -0
- package/control-panel.html +36 -12
- package/control-panel.js +13 -1
- package/control-policy.js +449 -0
- package/gateway.cjs +429 -89
- package/package.json +2 -2
- package/room-gateway.html +2 -2
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
const CONTROL_POLICY_MARKER = '/* ui-control-panel-control-api-v1 */'
|
|
2
|
+
const FEEDBACK_TOPIC_FIELDS = Object.freeze({
|
|
3
|
+
'display/power': 'display',
|
|
4
|
+
'display/source': 'source',
|
|
5
|
+
'audio/volume': 'volume',
|
|
6
|
+
'audio/mute': 'muted',
|
|
7
|
+
'lighting/brightness': 'light',
|
|
8
|
+
'room/curtain': 'curtains',
|
|
9
|
+
'climate/temperature': 'temperature',
|
|
10
|
+
'room/scene': 'scene',
|
|
11
|
+
'room/meeting': 'meeting'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function normalizeControlFeedback (message, expectedRoom, topicFields) {
|
|
15
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) {
|
|
16
|
+
throw new Error('Control feedback must be an object')
|
|
17
|
+
}
|
|
18
|
+
if (message.kind !== 'state') throw new Error('Control feedback must have kind=state')
|
|
19
|
+
if (expectedRoom && message.room !== expectedRoom) throw new Error('Control feedback room mismatch')
|
|
20
|
+
if (typeof message.topic !== 'string' || !message.topic) throw new Error('Control feedback needs a topic')
|
|
21
|
+
|
|
22
|
+
let state = message.state && typeof message.state === 'object' && !Array.isArray(message.state)
|
|
23
|
+
? { ...message.state }
|
|
24
|
+
: {}
|
|
25
|
+
if (message.topic === 'room/state') {
|
|
26
|
+
if (!message.payload || typeof message.payload !== 'object' || Array.isArray(message.payload)) {
|
|
27
|
+
throw new Error('room/state feedback needs an object payload')
|
|
28
|
+
}
|
|
29
|
+
state = { ...state, ...message.payload }
|
|
30
|
+
} else if (topicFields[message.topic]) {
|
|
31
|
+
state[topicFields[message.topic]] = message.payload
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
room: message.room,
|
|
36
|
+
kind: 'state',
|
|
37
|
+
topic: message.topic,
|
|
38
|
+
payload: message.payload,
|
|
39
|
+
state,
|
|
40
|
+
revision: Number.isInteger(message.revision) && message.revision >= 0 ? message.revision : null,
|
|
41
|
+
controlKey: typeof message.controlKey === 'string' ? message.controlKey : null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createControlPanelPolicy ({
|
|
46
|
+
dispatch,
|
|
47
|
+
intervalMs = 40,
|
|
48
|
+
now = () => Date.now(),
|
|
49
|
+
setTimer = setTimeout,
|
|
50
|
+
clearTimer = clearTimeout,
|
|
51
|
+
onError = () => {}
|
|
52
|
+
}) {
|
|
53
|
+
if (typeof dispatch !== 'function') throw new Error('ControlPanel dispatch function is required')
|
|
54
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 0) throw new Error('ControlPanel interval must be a non-negative number')
|
|
55
|
+
|
|
56
|
+
const continuousTypes = new Set(['level', 'dimmer', 'position'])
|
|
57
|
+
const discreteTypes = new Set(['power', 'source', 'preset', 'mute'])
|
|
58
|
+
const continuousControls = new Map()
|
|
59
|
+
let discreteQueue = Promise.resolve()
|
|
60
|
+
let disposed = false
|
|
61
|
+
|
|
62
|
+
const reportError = (error, command) => {
|
|
63
|
+
try {
|
|
64
|
+
onError(error, command)
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalize = (command) => {
|
|
69
|
+
if (!command || typeof command !== 'object' || Array.isArray(command)) {
|
|
70
|
+
throw new Error('Control command must be an object')
|
|
71
|
+
}
|
|
72
|
+
const type = String(command.type ?? command.controlType ?? '').trim().toLowerCase()
|
|
73
|
+
const topic = String(command.topic ?? '').trim()
|
|
74
|
+
const key = String(command.key ?? command.controlKey ?? '').trim()
|
|
75
|
+
const hasValue = Object.prototype.hasOwnProperty.call(command, 'value') ||
|
|
76
|
+
Object.prototype.hasOwnProperty.call(command, 'payload')
|
|
77
|
+
|
|
78
|
+
if (!continuousTypes.has(type) && !discreteTypes.has(type)) {
|
|
79
|
+
throw new Error('Unsupported control type: ' + type)
|
|
80
|
+
}
|
|
81
|
+
if (!topic) throw new Error('Control command needs a topic')
|
|
82
|
+
if (!/^[a-zA-Z0-9._:/-]{1,160}$/.test(key)) throw new Error('Control command needs a valid key')
|
|
83
|
+
if (!hasValue) throw new Error('Control command needs a value')
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
topic,
|
|
87
|
+
payload: Object.prototype.hasOwnProperty.call(command, 'value') ? command.value : command.payload,
|
|
88
|
+
controlType: type,
|
|
89
|
+
controlKey: key,
|
|
90
|
+
final: command.final === true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const continuousControl = (key) => {
|
|
95
|
+
let control = continuousControls.get(key)
|
|
96
|
+
if (!control) {
|
|
97
|
+
control = {
|
|
98
|
+
inFlight: false,
|
|
99
|
+
lastCommand: null,
|
|
100
|
+
lastSentAt: -intervalMs,
|
|
101
|
+
latest: null,
|
|
102
|
+
timer: null
|
|
103
|
+
}
|
|
104
|
+
continuousControls.set(key, control)
|
|
105
|
+
}
|
|
106
|
+
return control
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dispatchContinuous = async (key) => {
|
|
110
|
+
const control = continuousControl(key)
|
|
111
|
+
if (control.inFlight || !control.latest) return
|
|
112
|
+
|
|
113
|
+
const command = control.latest
|
|
114
|
+
control.latest = null
|
|
115
|
+
control.inFlight = true
|
|
116
|
+
try {
|
|
117
|
+
await dispatch(command)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
reportError(error, command)
|
|
120
|
+
} finally {
|
|
121
|
+
control.lastSentAt = now()
|
|
122
|
+
control.inFlight = false
|
|
123
|
+
if (!disposed && control.latest) scheduleContinuous(key, control.latest.final)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const scheduleContinuous = (key, force) => {
|
|
128
|
+
const control = continuousControl(key)
|
|
129
|
+
if (control.inFlight || !control.latest) return
|
|
130
|
+
if (force && control.timer) {
|
|
131
|
+
clearTimer(control.timer)
|
|
132
|
+
control.timer = null
|
|
133
|
+
}
|
|
134
|
+
if (control.timer) return
|
|
135
|
+
|
|
136
|
+
const delay = force ? 0 : Math.max(0, intervalMs - (now() - control.lastSentAt))
|
|
137
|
+
if (delay === 0) {
|
|
138
|
+
void dispatchContinuous(key)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
control.timer = setTimer(() => {
|
|
142
|
+
control.timer = null
|
|
143
|
+
void dispatchContinuous(key)
|
|
144
|
+
}, delay)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sendDiscrete = (command) => {
|
|
148
|
+
discreteQueue = discreteQueue
|
|
149
|
+
.catch(() => undefined)
|
|
150
|
+
.then(() => disposed ? undefined : dispatch(command))
|
|
151
|
+
.catch((error) => reportError(error, command))
|
|
152
|
+
return discreteQueue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
send (command) {
|
|
157
|
+
if (disposed) return Promise.resolve()
|
|
158
|
+
const normalized = normalize(command)
|
|
159
|
+
if (continuousTypes.has(normalized.controlType)) {
|
|
160
|
+
const control = continuousControl(normalized.controlKey)
|
|
161
|
+
control.lastCommand = normalized
|
|
162
|
+
control.latest = normalized
|
|
163
|
+
scheduleContinuous(normalized.controlKey, normalized.final)
|
|
164
|
+
return Promise.resolve()
|
|
165
|
+
}
|
|
166
|
+
return sendDiscrete(normalized)
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
flush (key) {
|
|
170
|
+
const control = continuousControls.get(String(key))
|
|
171
|
+
if (disposed || !control || !control.lastCommand) return Promise.resolve()
|
|
172
|
+
control.latest = { ...control.lastCommand, final: true }
|
|
173
|
+
scheduleContinuous(control.latest.controlKey, true)
|
|
174
|
+
return Promise.resolve()
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
dispose () {
|
|
178
|
+
disposed = true
|
|
179
|
+
for (const control of continuousControls.values()) {
|
|
180
|
+
if (control.timer) clearTimer(control.timer)
|
|
181
|
+
control.latest = null
|
|
182
|
+
}
|
|
183
|
+
continuousControls.clear()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildControlPolicyMixinSource () {
|
|
189
|
+
return `
|
|
190
|
+
${CONTROL_POLICY_MARKER}
|
|
191
|
+
const __controlPanelCreatePolicy = ${createControlPanelPolicy.toString()}
|
|
192
|
+
const __controlPanelFeedbackFields = ${JSON.stringify(FEEDBACK_TOPIC_FIELDS)}
|
|
193
|
+
const __controlPanelNormalizeFeedback = ${normalizeControlFeedback.toString()}
|
|
194
|
+
const __controlPanelPolicyMixin = {
|
|
195
|
+
data () {
|
|
196
|
+
return {
|
|
197
|
+
deviceState: {},
|
|
198
|
+
deviceFeedbackRevision: 0,
|
|
199
|
+
deviceFeedbackConnected: false,
|
|
200
|
+
controlPending: {},
|
|
201
|
+
controlTargets: {}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
created () {
|
|
205
|
+
this.__controlPanelSession = null
|
|
206
|
+
this.__controlPanelFeedbackSource = null
|
|
207
|
+
this.__controlPanelRequests = new Map()
|
|
208
|
+
this.__resetControlPolicy()
|
|
209
|
+
},
|
|
210
|
+
methods: {
|
|
211
|
+
__resetControlPolicy () {
|
|
212
|
+
this.__controlPanelPolicy?.dispose()
|
|
213
|
+
this.__controlPanelPolicy = __controlPanelCreatePolicy({
|
|
214
|
+
intervalMs: 40,
|
|
215
|
+
dispatch: (command) => this.__dispatchControl(command),
|
|
216
|
+
onError: (error, command) => {
|
|
217
|
+
if (command?.controlKey) this.__clearControlPending(command.controlKey)
|
|
218
|
+
if (typeof this.onControlError === 'function') this.onControlError(error, command)
|
|
219
|
+
else console.error('ControlPanel command failed', error)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
},
|
|
223
|
+
__setControlPending (key, pending) {
|
|
224
|
+
const next = { ...this.controlPending }
|
|
225
|
+
if (pending) next[key] = true
|
|
226
|
+
else delete next[key]
|
|
227
|
+
this.controlPending = next
|
|
228
|
+
},
|
|
229
|
+
__setControlTarget (key, value) {
|
|
230
|
+
this.controlTargets = { ...this.controlTargets, [key]: value }
|
|
231
|
+
},
|
|
232
|
+
__clearControlPending (key) {
|
|
233
|
+
this.__controlPanelRequests.delete(String(key))
|
|
234
|
+
this.__setControlPending(String(key), false)
|
|
235
|
+
const targets = { ...this.controlTargets }
|
|
236
|
+
delete targets[String(key)]
|
|
237
|
+
this.controlTargets = targets
|
|
238
|
+
},
|
|
239
|
+
__setFeedbackConnected (connected) {
|
|
240
|
+
const next = connected === true
|
|
241
|
+
if (this.deviceFeedbackConnected === next) return
|
|
242
|
+
this.deviceFeedbackConnected = next
|
|
243
|
+
if (typeof this.onControlFeedbackStatus === 'function') this.onControlFeedbackStatus(next)
|
|
244
|
+
},
|
|
245
|
+
__closeControlFeedback () {
|
|
246
|
+
this.__controlPanelFeedbackSource?.close()
|
|
247
|
+
this.__controlPanelFeedbackSource = null
|
|
248
|
+
this.__setFeedbackConnected(false)
|
|
249
|
+
},
|
|
250
|
+
__connectControlFeedback () {
|
|
251
|
+
this.__closeControlFeedback()
|
|
252
|
+
const session = this.__controlPanelSession
|
|
253
|
+
if (!session || typeof EventSource === 'undefined') return
|
|
254
|
+
const source = new EventSource('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/events')
|
|
255
|
+
this.__controlPanelFeedbackSource = source
|
|
256
|
+
source.onopen = () => {
|
|
257
|
+
if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(true)
|
|
258
|
+
}
|
|
259
|
+
source.onmessage = (event) => {
|
|
260
|
+
if (this.__controlPanelFeedbackSource !== source) return
|
|
261
|
+
try {
|
|
262
|
+
this.applyControlFeedback(JSON.parse(event.data))
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (typeof this.onControlError === 'function') this.onControlError(error)
|
|
265
|
+
else console.error('ControlPanel feedback failed', error)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
source.onerror = () => {
|
|
269
|
+
if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(false)
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
__acknowledgeControlFeedback (feedback) {
|
|
273
|
+
for (const [key, request] of this.__controlPanelRequests) {
|
|
274
|
+
if (feedback.controlKey && feedback.controlKey !== key) continue
|
|
275
|
+
let matched = request.topic === 'room/reset'
|
|
276
|
+
let value
|
|
277
|
+
if (!matched && feedback.topic === request.topic) {
|
|
278
|
+
matched = true
|
|
279
|
+
value = feedback.payload
|
|
280
|
+
} else if (!matched && feedback.topic === 'room/state') {
|
|
281
|
+
const field = __controlPanelFeedbackFields[request.topic]
|
|
282
|
+
if (field && Object.prototype.hasOwnProperty.call(feedback.state, field)) {
|
|
283
|
+
matched = true
|
|
284
|
+
value = feedback.state[field]
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (matched && (request.topic === 'room/reset' || Object.is(value, request.value))) {
|
|
288
|
+
this.__clearControlPending(key)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
setControlSession (session) {
|
|
293
|
+
if (!session || typeof session !== 'object' || !session.room || !session.csrf) {
|
|
294
|
+
throw new Error('ControlPanel session needs room and csrf')
|
|
295
|
+
}
|
|
296
|
+
const next = { room: String(session.room), csrf: String(session.csrf) }
|
|
297
|
+
if (this.__controlPanelSession &&
|
|
298
|
+
(this.__controlPanelSession.room !== next.room || this.__controlPanelSession.csrf !== next.csrf)) {
|
|
299
|
+
this.__resetControlPolicy()
|
|
300
|
+
this.__controlPanelRequests.clear()
|
|
301
|
+
this.controlPending = {}
|
|
302
|
+
this.controlTargets = {}
|
|
303
|
+
this.deviceState = {}
|
|
304
|
+
this.deviceFeedbackRevision = 0
|
|
305
|
+
}
|
|
306
|
+
this.__controlPanelSession = next
|
|
307
|
+
this.__connectControlFeedback()
|
|
308
|
+
},
|
|
309
|
+
clearControlSession () {
|
|
310
|
+
this.__closeControlFeedback()
|
|
311
|
+
this.__controlPanelSession = null
|
|
312
|
+
this.__resetControlPolicy()
|
|
313
|
+
this.__controlPanelRequests.clear()
|
|
314
|
+
this.controlPending = {}
|
|
315
|
+
this.controlTargets = {}
|
|
316
|
+
this.deviceState = {}
|
|
317
|
+
this.deviceFeedbackRevision = 0
|
|
318
|
+
},
|
|
319
|
+
sendControl (command) {
|
|
320
|
+
const key = String(command?.key ?? command?.controlKey ?? '')
|
|
321
|
+
const type = String(command?.type ?? command?.controlType ?? '').toLowerCase()
|
|
322
|
+
const value = Object.prototype.hasOwnProperty.call(command || {}, 'value') ? command.value : command?.payload
|
|
323
|
+
if (key) {
|
|
324
|
+
this.__controlPanelRequests.set(key, {
|
|
325
|
+
topic: String(command.topic ?? ''),
|
|
326
|
+
type,
|
|
327
|
+
value
|
|
328
|
+
})
|
|
329
|
+
this.__setControlPending(key, true)
|
|
330
|
+
if (['level', 'dimmer', 'position'].includes(type)) this.__setControlTarget(key, value)
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
return this.__controlPanelPolicy.send(command)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (key) this.__clearControlPending(key)
|
|
336
|
+
throw error
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
flushControl (key) {
|
|
340
|
+
return this.__controlPanelPolicy.flush(key)
|
|
341
|
+
},
|
|
342
|
+
applyControlFeedback (message) {
|
|
343
|
+
const room = this.__controlPanelSession?.room || this.authorizedRoom
|
|
344
|
+
const feedback = __controlPanelNormalizeFeedback(message, room, __controlPanelFeedbackFields)
|
|
345
|
+
this.deviceState = { ...this.deviceState, ...feedback.state }
|
|
346
|
+
if (feedback.revision !== null) this.deviceFeedbackRevision = feedback.revision
|
|
347
|
+
this.__acknowledgeControlFeedback(feedback)
|
|
348
|
+
if (typeof this.onDeviceFeedback === 'function') this.onDeviceFeedback(feedback, this.deviceState)
|
|
349
|
+
return feedback
|
|
350
|
+
},
|
|
351
|
+
hasDeviceState (field) {
|
|
352
|
+
return Object.prototype.hasOwnProperty.call(this.deviceState, String(field))
|
|
353
|
+
},
|
|
354
|
+
deviceValue (field, fallback = null) {
|
|
355
|
+
return this.hasDeviceState(field) ? this.deviceState[String(field)] : fallback
|
|
356
|
+
},
|
|
357
|
+
isControlPending (key) {
|
|
358
|
+
return this.controlPending[String(key)] === true
|
|
359
|
+
},
|
|
360
|
+
controlDisplayValue (key, field, fallback = null) {
|
|
361
|
+
const controlKey = String(key)
|
|
362
|
+
if (this.isControlPending(controlKey) &&
|
|
363
|
+
Object.prototype.hasOwnProperty.call(this.controlTargets, controlKey)) {
|
|
364
|
+
return this.controlTargets[controlKey]
|
|
365
|
+
}
|
|
366
|
+
return this.deviceValue(field, fallback)
|
|
367
|
+
},
|
|
368
|
+
async __dispatchControl (command) {
|
|
369
|
+
if (typeof this.controlTransport === 'function') return this.controlTransport(command)
|
|
370
|
+
|
|
371
|
+
const session = this.__controlPanelSession
|
|
372
|
+
const room = session?.room || this.authorizedRoom
|
|
373
|
+
if (typeof this.postControl === 'function') {
|
|
374
|
+
return this.postControl(room ? { ...command, room } : command, this.sessionGeneration)
|
|
375
|
+
}
|
|
376
|
+
if (!session) throw new Error('Call setControlSession() after login, or define controlTransport(command)')
|
|
377
|
+
|
|
378
|
+
const response = await fetch('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/command', {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
credentials: 'same-origin',
|
|
381
|
+
headers: { 'Content-Type': 'application/json', 'X-Room-CSRF': session.csrf },
|
|
382
|
+
body: JSON.stringify({ room: session.room, ...command }),
|
|
383
|
+
signal: AbortSignal.timeout(8000)
|
|
384
|
+
})
|
|
385
|
+
if (!response.ok) throw new Error('Control command failed (' + response.status + ')')
|
|
386
|
+
return response
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
unmounted () {
|
|
390
|
+
this.__closeControlFeedback()
|
|
391
|
+
this.__controlPanelPolicy?.dispose()
|
|
392
|
+
this.__controlPanelPolicy = null
|
|
393
|
+
this.__controlPanelRequests?.clear()
|
|
394
|
+
this.controlPending = {}
|
|
395
|
+
this.controlTargets = {}
|
|
396
|
+
this.__controlPanelSession = null
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
`
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function injectControlPolicy (format) {
|
|
403
|
+
if (typeof format !== 'string' || format.includes(CONTROL_POLICY_MARKER)) return format
|
|
404
|
+
|
|
405
|
+
const scriptMatch = /<script(?!\s+setup\b)(?:\s[^>]*)?>/i.exec(format)
|
|
406
|
+
if (!scriptMatch) {
|
|
407
|
+
const setupMatch = /<script\s+setup\b([^>]*)>/i.exec(format)
|
|
408
|
+
if (!setupMatch && !/<template(?:\s|>)/i.test(format)) return format
|
|
409
|
+
const langMatch = setupMatch?.[1]?.match(/\blang\s*=\s*(["'])([^"']+)\1/i)
|
|
410
|
+
const scriptOpen = langMatch ? '<script lang="' + langMatch[2] + '">' : '<script>'
|
|
411
|
+
return format + '\n' + scriptOpen + '\n' + buildControlPolicyMixinSource() +
|
|
412
|
+
'\nexport default { mixins: [__controlPanelPolicyMixin] }\n</script>\n'
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const scriptStart = scriptMatch.index + scriptMatch[0].length
|
|
416
|
+
const scriptEnd = format.indexOf('</script>', scriptStart)
|
|
417
|
+
if (scriptEnd === -1) return format
|
|
418
|
+
|
|
419
|
+
const source = format.slice(scriptStart, scriptEnd)
|
|
420
|
+
const exportMatch = /(^|\n)([ \t]*)export\s+default\b/.exec(source)
|
|
421
|
+
if (!exportMatch) {
|
|
422
|
+
const wrapped = source + '\n' + buildControlPolicyMixinSource() +
|
|
423
|
+
'\nexport default { mixins: [__controlPanelPolicyMixin] }\n'
|
|
424
|
+
return format.slice(0, scriptStart) + wrapped + format.slice(scriptEnd)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const exportStart = exportMatch.index + exportMatch[1].length + exportMatch[2].length
|
|
428
|
+
const exportEnd = exportMatch.index + exportMatch[0].length
|
|
429
|
+
const userOptions = source.slice(0, exportStart) +
|
|
430
|
+
'const __controlPanelUserOptions =' +
|
|
431
|
+
source.slice(exportEnd)
|
|
432
|
+
const wrapped = userOptions + '\n' + buildControlPolicyMixinSource() + `
|
|
433
|
+
export default {
|
|
434
|
+
...__controlPanelUserOptions,
|
|
435
|
+
mixins: [...(__controlPanelUserOptions.mixins || []), __controlPanelPolicyMixin]
|
|
436
|
+
}
|
|
437
|
+
`
|
|
438
|
+
|
|
439
|
+
return format.slice(0, scriptStart) + wrapped + format.slice(scriptEnd)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
module.exports = {
|
|
443
|
+
CONTROL_POLICY_MARKER,
|
|
444
|
+
FEEDBACK_TOPIC_FIELDS,
|
|
445
|
+
buildControlPolicyMixinSource,
|
|
446
|
+
createControlPanelPolicy,
|
|
447
|
+
injectControlPolicy,
|
|
448
|
+
normalizeControlFeedback
|
|
449
|
+
}
|