helpdesk-app-framework-sdk 1.0.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/LICENSE +177 -0
- package/README.md +587 -0
- package/build/baf_sdk.js +2107 -0
- package/build/baf_sdk.js.map +1 -0
- package/build/baf_sdk.min.js +1 -0
- package/lib/apis/context.js +172 -0
- package/lib/apis/instance.js +225 -0
- package/lib/apis/metadata.js +156 -0
- package/lib/apis/request.js +196 -0
- package/lib/client.js +422 -0
- package/lib/index.js +99 -0
- package/lib/utils.js +202 -0
- package/package.json +95 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoldDesk App Framework - Client SDK
|
|
3
|
+
* Core client for cross-frame communication and data access
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { queryParameters, when, isObject, isString, isArray, isFunction, debounce, deepClone } from './utils'
|
|
7
|
+
import ContextAPI from './apis/context'
|
|
8
|
+
import MetadataAPI from './apis/metadata'
|
|
9
|
+
import { InstanceAPI } from './apis/instance'
|
|
10
|
+
import RequestAPI from './apis/request'
|
|
11
|
+
import NativePromise from 'native-promise-only'
|
|
12
|
+
|
|
13
|
+
const Promise = window.Promise || NativePromise
|
|
14
|
+
|
|
15
|
+
// Constants
|
|
16
|
+
const IFRAME_SESSION_TIMEOUT = 5 * 60 * 1000
|
|
17
|
+
const PROMISE_TIMEOUT = 10000
|
|
18
|
+
const PROMISE_TIMEOUT_LONG = 5 * 60 * 1000
|
|
19
|
+
|
|
20
|
+
class Client {
|
|
21
|
+
constructor (options = {}) {
|
|
22
|
+
this.origin = options.origin
|
|
23
|
+
this.appGuid = options.appGuid
|
|
24
|
+
|
|
25
|
+
// Initialize state
|
|
26
|
+
this._isReady = false
|
|
27
|
+
this._messageQueue = []
|
|
28
|
+
this._pendingRequests = {}
|
|
29
|
+
this._eventListeners = {}
|
|
30
|
+
this._requestId = 0
|
|
31
|
+
this._currentInstanceId = null
|
|
32
|
+
|
|
33
|
+
// Initialize APIs
|
|
34
|
+
this.context = new ContextAPI()
|
|
35
|
+
this.metadata = new MetadataAPI()
|
|
36
|
+
this.instance = new InstanceAPI(this)
|
|
37
|
+
this.request = new RequestAPI(this)
|
|
38
|
+
|
|
39
|
+
// Bind message handler
|
|
40
|
+
this._handleMessage = this._handleMessage.bind(this)
|
|
41
|
+
window.addEventListener('message', this._handleMessage)
|
|
42
|
+
|
|
43
|
+
// Register with host
|
|
44
|
+
this._registerWithHost()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register app with BoldDesk host
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
_registerWithHost () {
|
|
52
|
+
this.postMessage('app.register', {
|
|
53
|
+
appGuid: this.appGuid,
|
|
54
|
+
version: '1.0.0'
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Post message to host frame
|
|
60
|
+
* @param {string} action - Action name
|
|
61
|
+
* @param {*} data - Data payload
|
|
62
|
+
* @param {Function} callback - Optional callback
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
postMessage (action, data, callback) {
|
|
66
|
+
const requestId = ++this._requestId
|
|
67
|
+
|
|
68
|
+
if (callback && isFunction(callback)) {
|
|
69
|
+
this._pendingRequests[requestId] = callback
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const message = {
|
|
73
|
+
type: 'baf.request',
|
|
74
|
+
action,
|
|
75
|
+
data,
|
|
76
|
+
requestId,
|
|
77
|
+
appGuid: this.appGuid,
|
|
78
|
+
timestamp: Date.now()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this._isReady) {
|
|
82
|
+
window.parent.postMessage(message, this.origin)
|
|
83
|
+
} else {
|
|
84
|
+
this._messageQueue.push(message)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handle incoming messages from host
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
_handleMessage (event) {
|
|
93
|
+
if (event.origin !== this.origin) return
|
|
94
|
+
|
|
95
|
+
const message = event.data
|
|
96
|
+
if (!message || message.type !== 'baf.response') return
|
|
97
|
+
|
|
98
|
+
// Handle app registration
|
|
99
|
+
if (message.action === 'app.registered') {
|
|
100
|
+
this._isReady = true
|
|
101
|
+
this._processMessageQueue()
|
|
102
|
+
this._handleAppRegistered(message.data)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle pending request responses
|
|
107
|
+
if (message.requestId && this._pendingRequests[message.requestId]) {
|
|
108
|
+
const callback = this._pendingRequests[message.requestId]
|
|
109
|
+
delete this._pendingRequests[message.requestId]
|
|
110
|
+
callback(message.data)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle events
|
|
115
|
+
if (message.action === 'event') {
|
|
116
|
+
this._handleEvent(message.eventName, message.data)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle instance messages
|
|
121
|
+
if (message.action === 'instance.message') {
|
|
122
|
+
this.instance._triggerMessage(message.channel, message.payload)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle instance registration
|
|
127
|
+
if (message.action === 'instance.registered') {
|
|
128
|
+
this.instance._registerInstance(message.data)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Handle app registration from host
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_handleAppRegistered (data) {
|
|
138
|
+
if (data.context) {
|
|
139
|
+
this.context._updateData(data.context)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (data.metadata) {
|
|
143
|
+
this.metadata._updateData(data.metadata)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (data.instanceId) {
|
|
147
|
+
this._currentInstanceId = data.instanceId
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Emit app.registered event
|
|
151
|
+
this._emitEvent('app.registered', {
|
|
152
|
+
context: this.context.getAll(),
|
|
153
|
+
metadata: this.metadata.getAll()
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Process queued messages
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
_processMessageQueue () {
|
|
162
|
+
while (this._messageQueue.length > 0) {
|
|
163
|
+
const message = this._messageQueue.shift()
|
|
164
|
+
window.parent.postMessage(message, this.origin)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get data from platform context
|
|
170
|
+
* Retrieves data like ticket info, user info, etc.
|
|
171
|
+
*
|
|
172
|
+
* @param {string|Array} path - Data path or array of paths
|
|
173
|
+
* @returns {Promise} Resolves with requested data
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* // Get single value
|
|
177
|
+
* sdk.get('ticket.subject').then(data => {
|
|
178
|
+
* console.log(data['ticket.subject']);
|
|
179
|
+
* });
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* // Get multiple values
|
|
183
|
+
* sdk.get(['ticket.subject', 'ticket.requester.email']).then(data => {
|
|
184
|
+
* console.log(data['ticket.subject']);
|
|
185
|
+
* console.log(data['ticket.requester.email']);
|
|
186
|
+
* });
|
|
187
|
+
*/
|
|
188
|
+
get (path) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
if (!path) {
|
|
191
|
+
return reject(new Error('Path is required for get() method'))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const paths = isArray(path) ? path : [path]
|
|
196
|
+
this.postMessage('data.get', { paths }, (response) => {
|
|
197
|
+
if (response.error) {
|
|
198
|
+
reject(new Error(response.error))
|
|
199
|
+
} else {
|
|
200
|
+
resolve(response.data || {})
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
} catch (error) {
|
|
204
|
+
reject(error)
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set data in platform context
|
|
211
|
+
* Updates or stores data like ticket properties, custom fields, etc.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} path - Data path to update
|
|
214
|
+
* @param {*} value - Value to set
|
|
215
|
+
* @returns {Promise} Resolves when data is set
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* sdk.set('ticket.subject', 'Issue Resolved').then(() => {
|
|
219
|
+
* console.log('Subject updated');
|
|
220
|
+
* });
|
|
221
|
+
*/
|
|
222
|
+
set (path, value) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
if (!path) {
|
|
225
|
+
return reject(new Error('Path is required for set() method'))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
this.postMessage('data.set', { path, value }, (response) => {
|
|
230
|
+
if (response.error) {
|
|
231
|
+
reject(new Error(response.error))
|
|
232
|
+
} else {
|
|
233
|
+
resolve()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
} catch (error) {
|
|
237
|
+
reject(error)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Register event listener
|
|
244
|
+
* Listen for platform events like data changes, UI events, etc.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} eventName - Event name to listen for
|
|
247
|
+
* @param {Function} callback - Handler function
|
|
248
|
+
* @returns {void}
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* sdk.on('ticket.updated', (ticket) => {
|
|
252
|
+
* console.log('Ticket updated:', ticket);
|
|
253
|
+
* });
|
|
254
|
+
*/
|
|
255
|
+
on (eventName, callback) {
|
|
256
|
+
if (!isString(eventName) || !isFunction(callback)) {
|
|
257
|
+
throw new Error('on() requires eventName (string) and callback (function)')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!this._eventListeners[eventName]) {
|
|
261
|
+
this._eventListeners[eventName] = []
|
|
262
|
+
this.postMessage('event.register', { eventName })
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this._eventListeners[eventName].push(callback)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Unregister event listener
|
|
270
|
+
* Stop listening for platform events
|
|
271
|
+
*
|
|
272
|
+
* @param {string} eventName - Event name
|
|
273
|
+
* @param {Function} callback - Optional specific handler to remove
|
|
274
|
+
* @returns {void}
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* sdk.off('ticket.updated');
|
|
278
|
+
*/
|
|
279
|
+
off (eventName, callback) {
|
|
280
|
+
if (!this._eventListeners[eventName]) return
|
|
281
|
+
|
|
282
|
+
if (callback && isFunction(callback)) {
|
|
283
|
+
const index = this._eventListeners[eventName].indexOf(callback)
|
|
284
|
+
if (index > -1) {
|
|
285
|
+
this._eventListeners[eventName].splice(index, 1)
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
this._eventListeners[eventName] = []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Unregister from host if no more listeners
|
|
292
|
+
if (this._eventListeners[eventName].length === 0) {
|
|
293
|
+
this.postMessage('event.unregister', { eventName })
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Invoke a platform action or method
|
|
299
|
+
* Execute platform-exposed methods like opening modals, etc.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} methodName - Method name to invoke
|
|
302
|
+
* @param {*} payload - Optional data for the method
|
|
303
|
+
* @returns {Promise} Resolves with method result
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* sdk.invoke('modal.open', {
|
|
307
|
+
* title: 'Contact Details',
|
|
308
|
+
* content: 'View full contact information'
|
|
309
|
+
* });
|
|
310
|
+
*/
|
|
311
|
+
invoke (methodName, payload) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
if (!isString(methodName)) {
|
|
314
|
+
return reject(new Error('Method name is required'))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
this.postMessage('action.invoke', { methodName, payload }, (response) => {
|
|
319
|
+
if (response.error) {
|
|
320
|
+
reject(new Error(response.error))
|
|
321
|
+
} else {
|
|
322
|
+
resolve(response.result)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
} catch (error) {
|
|
326
|
+
reject(error)
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Trigger a custom event
|
|
333
|
+
* Emit custom events to the platform or other app components
|
|
334
|
+
*
|
|
335
|
+
* @param {string} eventName - Event name
|
|
336
|
+
* @param {*} data - Event data
|
|
337
|
+
* @returns {void}
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* sdk.trigger('custom.notification', {
|
|
341
|
+
* message: 'Data saved successfully'
|
|
342
|
+
* });
|
|
343
|
+
*/
|
|
344
|
+
trigger (eventName, data) {
|
|
345
|
+
if (!isString(eventName)) {
|
|
346
|
+
throw new Error('Event name must be a string')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this.postMessage('event.trigger', { eventName, data })
|
|
350
|
+
|
|
351
|
+
// Also emit locally
|
|
352
|
+
this._emitEvent(eventName, data)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if capability or value exists
|
|
357
|
+
* Verify availability of features or data paths
|
|
358
|
+
*
|
|
359
|
+
* @param {string} name - Capability or value name
|
|
360
|
+
* @returns {Promise} Resolves with boolean
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* sdk.has('ticket.custom_field_123').then(exists => {
|
|
364
|
+
* if (exists) {
|
|
365
|
+
* console.log('Custom field is available');
|
|
366
|
+
* }
|
|
367
|
+
* });
|
|
368
|
+
*/
|
|
369
|
+
has (name) {
|
|
370
|
+
return new Promise((resolve, reject) => {
|
|
371
|
+
if (!isString(name)) {
|
|
372
|
+
return reject(new Error('Name must be a string'))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
this.postMessage('capability.check', { name }, (response) => {
|
|
377
|
+
resolve(response.exists === true)
|
|
378
|
+
})
|
|
379
|
+
} catch (error) {
|
|
380
|
+
reject(error)
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Internal: Emit event to local listeners
|
|
387
|
+
* @private
|
|
388
|
+
*/
|
|
389
|
+
_emitEvent (eventName, data) {
|
|
390
|
+
if (this._eventListeners[eventName]) {
|
|
391
|
+
this._eventListeners[eventName].forEach(callback => {
|
|
392
|
+
try {
|
|
393
|
+
callback(data)
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error(`Error in event listener for ${eventName}:`, error)
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Internal: Handle incoming event from host
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
405
|
+
_handleEvent (eventName, data) {
|
|
406
|
+
this._emitEvent(eventName, data)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Destroy client and clean up
|
|
411
|
+
* @returns {void}
|
|
412
|
+
*/
|
|
413
|
+
destroy () {
|
|
414
|
+
window.removeEventListener('message', this._handleMessage)
|
|
415
|
+
this._eventListeners = {}
|
|
416
|
+
this._pendingRequests = {}
|
|
417
|
+
this._messageQueue = []
|
|
418
|
+
this._isReady = false
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export default Client
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoldDesk App Framework SDK
|
|
3
|
+
* Main entry point for the BAF SDK library
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Client from './client'
|
|
7
|
+
import { queryParameters } from './utils'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* BAFClient - BoldDesk App Framework Client
|
|
11
|
+
*
|
|
12
|
+
* Main API for initializing and using the BoldDesk App Framework SDK
|
|
13
|
+
* in embedded app iframes.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Initialize the client
|
|
17
|
+
* const client = BAFClient.init((context) => {
|
|
18
|
+
* console.log('App initialized');
|
|
19
|
+
* console.log('Current module:', context.module);
|
|
20
|
+
* console.log('User:', context.user.name);
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // Use the client APIs
|
|
24
|
+
* client.get('ticket.subject').then(data => {
|
|
25
|
+
* console.log(data['ticket.subject']);
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const BAFClient = {
|
|
30
|
+
/**
|
|
31
|
+
* Initialize BAFClient and establish connection with BoldDesk host
|
|
32
|
+
*
|
|
33
|
+
* Call this function in your app's HTML to initialize the SDK.
|
|
34
|
+
* The client will automatically detect the app instance and establish
|
|
35
|
+
* two-way communication with the BoldDesk platform.
|
|
36
|
+
*
|
|
37
|
+
* @param {Function} callback - Optional callback function called when app is registered
|
|
38
|
+
* Receives context object with app information
|
|
39
|
+
* @param {Object} loc - Optional window location object (for testing)
|
|
40
|
+
* @returns {Client|boolean} Client instance or false if not in iframe
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Basic usage
|
|
44
|
+
* const client = BAFClient.init();
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // With callback
|
|
48
|
+
* const client = BAFClient.init((context) => {
|
|
49
|
+
* const currentUser = context.user;
|
|
50
|
+
* console.log('Hello ' + currentUser.name);
|
|
51
|
+
* });
|
|
52
|
+
*/
|
|
53
|
+
init: function (callback, loc) {
|
|
54
|
+
loc = loc || window.location
|
|
55
|
+
|
|
56
|
+
// Parse query/hash parameters
|
|
57
|
+
const queryParams = queryParameters(loc.search)
|
|
58
|
+
const hashParams = queryParameters(loc.hash)
|
|
59
|
+
|
|
60
|
+
// Required parameters for iframe app
|
|
61
|
+
const origin = queryParams.origin || hashParams.origin
|
|
62
|
+
const appGuid = queryParams.app_guid || hashParams.app_guid
|
|
63
|
+
|
|
64
|
+
// Validate required parameters
|
|
65
|
+
if (!origin || !appGuid) {
|
|
66
|
+
console.error(
|
|
67
|
+
'BAFClient.init(): Missing required parameters. ' +
|
|
68
|
+
'App must be loaded with "origin" and "app_guid" parameters.'
|
|
69
|
+
)
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create client instance
|
|
74
|
+
const client = new Client({ origin, appGuid })
|
|
75
|
+
|
|
76
|
+
// Register callback for app registration event
|
|
77
|
+
if (typeof callback === 'function') {
|
|
78
|
+
client.on('app.registered', function (context) {
|
|
79
|
+
callback.call(client, context)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return client
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default BAFClient
|
|
88
|
+
|
|
89
|
+
// Also export Client for advanced usage
|
|
90
|
+
export { default as Client } from './client'
|
|
91
|
+
|
|
92
|
+
// Export utility functions
|
|
93
|
+
export * from './utils'
|
|
94
|
+
|
|
95
|
+
// Export APIs for direct access
|
|
96
|
+
export { default as ContextAPI } from './apis/context'
|
|
97
|
+
export { default as MetadataAPI } from './apis/metadata'
|
|
98
|
+
export { InstanceAPI } from './apis/instance'
|
|
99
|
+
export { default as RequestAPI } from './apis/request'
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoldDesk App Framework - Utility Functions
|
|
3
|
+
* Provides helper functions for common operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Query string parser
|
|
8
|
+
* @param {string} queryString - Query string to parse (with or without ?)
|
|
9
|
+
* @returns {Object} Parsed parameters
|
|
10
|
+
*/
|
|
11
|
+
export function queryParameters (queryString) {
|
|
12
|
+
const params = {}
|
|
13
|
+
if (!queryString) return params
|
|
14
|
+
|
|
15
|
+
const cleaned = queryString.replace(/^\?|^#/, '')
|
|
16
|
+
if (!cleaned) return params
|
|
17
|
+
|
|
18
|
+
cleaned.split('&').forEach((pair) => {
|
|
19
|
+
const [key, value] = pair.split('=')
|
|
20
|
+
if (key) {
|
|
21
|
+
params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : true
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return params
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Promise utility - when all conditions are met
|
|
30
|
+
* @param {Array} promises - Array of promises to wait for
|
|
31
|
+
* @returns {Promise}
|
|
32
|
+
*/
|
|
33
|
+
export function when (promises) {
|
|
34
|
+
return Promise.all(promises)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Type checking utilities
|
|
39
|
+
*/
|
|
40
|
+
export const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]'
|
|
41
|
+
export const isString = (str) => typeof str === 'string'
|
|
42
|
+
export const isArray = (arr) => Array.isArray(arr)
|
|
43
|
+
export const isFunction = (fn) => typeof fn === 'function'
|
|
44
|
+
export const isUndefined = (val) => typeof val === 'undefined'
|
|
45
|
+
export const isNull = (val) => val === null
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Debounce function - delays execution
|
|
49
|
+
* @param {Function} fn - Function to debounce
|
|
50
|
+
* @param {number} delay - Delay in milliseconds
|
|
51
|
+
* @returns {Function} Debounced function
|
|
52
|
+
*/
|
|
53
|
+
export function debounce (fn, delay) {
|
|
54
|
+
let timeoutId
|
|
55
|
+
return function (...args) {
|
|
56
|
+
clearTimeout(timeoutId)
|
|
57
|
+
timeoutId = setTimeout(() => fn.apply(this, args), delay)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Throttle function - rate limits execution
|
|
63
|
+
* @param {Function} fn - Function to throttle
|
|
64
|
+
* @param {number} limit - Time limit in milliseconds
|
|
65
|
+
* @returns {Function} Throttled function
|
|
66
|
+
*/
|
|
67
|
+
export function throttle (fn, limit) {
|
|
68
|
+
let inThrottle
|
|
69
|
+
return function (...args) {
|
|
70
|
+
if (!inThrottle) {
|
|
71
|
+
fn.apply(this, args)
|
|
72
|
+
inThrottle = true
|
|
73
|
+
setTimeout(() => (inThrottle = false), limit)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Deep clone an object
|
|
80
|
+
* @param {*} obj - Object to clone
|
|
81
|
+
* @returns {*} Cloned object
|
|
82
|
+
*/
|
|
83
|
+
export function deepClone (obj) {
|
|
84
|
+
if (obj === null || typeof obj !== 'object') return obj
|
|
85
|
+
if (obj instanceof Date) return new Date(obj.getTime())
|
|
86
|
+
if (obj instanceof Array) return obj.map(item => deepClone(item))
|
|
87
|
+
if (obj instanceof Object) {
|
|
88
|
+
const cloned = {}
|
|
89
|
+
for (const key in obj) {
|
|
90
|
+
if (obj.hasOwnProperty(key)) {
|
|
91
|
+
cloned[key] = deepClone(obj[key])
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return cloned
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Merge objects deeply
|
|
100
|
+
* @param {Object} target - Target object
|
|
101
|
+
* @param {Object} source - Source object
|
|
102
|
+
* @returns {Object} Merged object
|
|
103
|
+
*/
|
|
104
|
+
export function deepMerge (target, source) {
|
|
105
|
+
const result = { ...target }
|
|
106
|
+
for (const key in source) {
|
|
107
|
+
if (source.hasOwnProperty(key)) {
|
|
108
|
+
if (isObject(source[key]) && isObject(result[key])) {
|
|
109
|
+
result[key] = deepMerge(result[key], source[key])
|
|
110
|
+
} else {
|
|
111
|
+
result[key] = deepClone(source[key])
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* UUID v4 generator
|
|
120
|
+
* @returns {string} Generated UUID
|
|
121
|
+
*/
|
|
122
|
+
export function generateUUID () {
|
|
123
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
124
|
+
const r = (Math.random() * 16) | 0
|
|
125
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
126
|
+
return v.toString(16)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate manifest fields
|
|
132
|
+
* @param {Object} manifest - Manifest object to validate
|
|
133
|
+
* @returns {Object} Validation result { valid: boolean, errors: Array }
|
|
134
|
+
*/
|
|
135
|
+
export function validateManifest (manifest) {
|
|
136
|
+
const errors = []
|
|
137
|
+
|
|
138
|
+
if (!manifest.name) errors.push('Manifest field "name" is required')
|
|
139
|
+
if (!manifest.version) errors.push('Manifest field "version" is required')
|
|
140
|
+
if (!manifest.frameworkVersion) errors.push('Manifest field "frameworkVersion" is required')
|
|
141
|
+
if (!manifest.product) errors.push('Manifest field "product" is required')
|
|
142
|
+
|
|
143
|
+
if (manifest.developer) {
|
|
144
|
+
const dev = manifest.developer
|
|
145
|
+
if (!dev.name) errors.push('Developer field "name" is required')
|
|
146
|
+
if (!dev.contactEmail) errors.push('Developer field "contactEmail" is required')
|
|
147
|
+
if (!dev.supportEmail) errors.push('Developer field "supportEmail" is required')
|
|
148
|
+
} else {
|
|
149
|
+
errors.push('Manifest field "developer" is required')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!Array.isArray(manifest.trustedDomains)) {
|
|
153
|
+
errors.push('Manifest field "trustedDomains" must be an array')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
valid: errors.length === 0,
|
|
158
|
+
errors
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parse widget locations to ensure valid format
|
|
164
|
+
* @param {Array} locations - Widget locations from manifest
|
|
165
|
+
* @returns {Array} Parsed locations
|
|
166
|
+
*/
|
|
167
|
+
export function parseWidgetLocations (locations) {
|
|
168
|
+
const validLocations = [
|
|
169
|
+
'desk.menu.left',
|
|
170
|
+
'desk.ticket.view.rightpanel',
|
|
171
|
+
'desk.contact.view.rightpanel',
|
|
172
|
+
'desk.contactgroup.view.rightpanel',
|
|
173
|
+
'desk.chat.view.rightpanel',
|
|
174
|
+
'desk.cti.widget'
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
return locations.filter(loc => validLocations.includes(loc))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate URL format
|
|
182
|
+
* @param {string} url - URL to validate
|
|
183
|
+
* @returns {boolean} Is valid URL
|
|
184
|
+
*/
|
|
185
|
+
export function isValidUrl (url) {
|
|
186
|
+
try {
|
|
187
|
+
new URL(url)
|
|
188
|
+
return true
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Format error message for logging
|
|
196
|
+
* @param {string} context - Context of error
|
|
197
|
+
* @param {Error} error - Error object
|
|
198
|
+
* @returns {string} Formatted error message
|
|
199
|
+
*/
|
|
200
|
+
export function formatError (context, error) {
|
|
201
|
+
return `[${context}] ${error.message || error}`
|
|
202
|
+
}
|