qdadm 0.29.0 → 0.31.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/package.json +11 -2
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/index.js +5 -3
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +2 -1
- package/src/composables/useAuth.js +43 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/composables/useSSEBridge.js +118 -0
- package/src/debug/AuthCollector.js +254 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +376 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +103 -0
- package/src/debug/components/panels/EntitiesPanel.vue +616 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/editors/index.js +12 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/storage/MockApiStorage.js +17 -1
- package/src/entity/storage/index.js +9 -2
- package/src/gen/index.js +3 -0
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +661 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SSEBridge.js +354 -0
- package/src/kernel/SignalBus.js +10 -7
- package/src/kernel/index.js +19 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- package/src/toast/useSignalToast.js +113 -0
- package/src/composables/useSSE.js +0 -212
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSEBridge - Server-Sent Events to SignalBus bridge
|
|
3
|
+
*
|
|
4
|
+
* Manages a single SSE connection and emits all events to SignalBus.
|
|
5
|
+
* Components subscribe via signals.on('sse:eventName', handler) instead of
|
|
6
|
+
* managing their own EventSource connections.
|
|
7
|
+
*
|
|
8
|
+
* Benefits:
|
|
9
|
+
* - Single SSE connection per app (vs. one per component)
|
|
10
|
+
* - Decoupled event handling via SignalBus
|
|
11
|
+
* - Automatic reconnection with configurable delay
|
|
12
|
+
* - Connection status available via signals
|
|
13
|
+
*
|
|
14
|
+
* Signal naming:
|
|
15
|
+
* - `sse:connected` - Emitted when connection established
|
|
16
|
+
* - `sse:disconnected` - Emitted when connection lost
|
|
17
|
+
* - `sse:error` - Emitted on connection error
|
|
18
|
+
* - `sse:{eventName}` - SSE event forwarded (e.g., sse:task:completed)
|
|
19
|
+
*
|
|
20
|
+
* Usage in Kernel:
|
|
21
|
+
* ```js
|
|
22
|
+
* new Kernel({
|
|
23
|
+
* sse: {
|
|
24
|
+
* url: '/api/events',
|
|
25
|
+
* reconnectDelay: 5000,
|
|
26
|
+
* signalPrefix: 'sse',
|
|
27
|
+
* autoConnect: true
|
|
28
|
+
* }
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Usage in components:
|
|
33
|
+
* ```js
|
|
34
|
+
* const signals = inject('qdadmSignals')
|
|
35
|
+
*
|
|
36
|
+
* // Subscribe to specific SSE events
|
|
37
|
+
* signals.on('sse:task:completed', ({ data }) => {
|
|
38
|
+
* console.log('Task completed:', data)
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // Subscribe to all SSE events via wildcard
|
|
42
|
+
* signals.on('sse:*', ({ event, data }) => {
|
|
43
|
+
* console.log(`SSE event ${event}:`, data)
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export const SSE_SIGNALS = {
|
|
49
|
+
CONNECTED: 'sse:connected',
|
|
50
|
+
DISCONNECTED: 'sse:disconnected',
|
|
51
|
+
ERROR: 'sse:error',
|
|
52
|
+
MESSAGE: 'sse:message'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class SSEBridge {
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} options
|
|
58
|
+
* @param {SignalBus} options.signals - SignalBus instance
|
|
59
|
+
* @param {string} options.url - SSE endpoint URL
|
|
60
|
+
* @param {number} options.reconnectDelay - Delay before reconnect (ms), 0 to disable
|
|
61
|
+
* @param {string} options.signalPrefix - Prefix for emitted signals (default: 'sse')
|
|
62
|
+
* @param {boolean} options.autoConnect - Connect immediately (default: false, waits for auth:login)
|
|
63
|
+
* @param {boolean} options.withCredentials - Include credentials in request
|
|
64
|
+
* @param {string} options.tokenParam - Query param name for auth token
|
|
65
|
+
* @param {function} options.getToken - Function to get auth token
|
|
66
|
+
* @param {boolean} options.debug - Enable debug logging
|
|
67
|
+
*/
|
|
68
|
+
constructor(options) {
|
|
69
|
+
const {
|
|
70
|
+
signals,
|
|
71
|
+
url,
|
|
72
|
+
reconnectDelay = 5000,
|
|
73
|
+
signalPrefix = 'sse',
|
|
74
|
+
autoConnect = false,
|
|
75
|
+
withCredentials = false,
|
|
76
|
+
tokenParam = 'token',
|
|
77
|
+
getToken = null,
|
|
78
|
+
debug = false
|
|
79
|
+
} = options
|
|
80
|
+
|
|
81
|
+
if (!signals) {
|
|
82
|
+
throw new Error('[SSEBridge] signals (SignalBus) is required')
|
|
83
|
+
}
|
|
84
|
+
if (!url) {
|
|
85
|
+
throw new Error('[SSEBridge] url is required')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._signals = signals
|
|
89
|
+
this._url = url
|
|
90
|
+
this._reconnectDelay = reconnectDelay
|
|
91
|
+
this._signalPrefix = signalPrefix
|
|
92
|
+
this._withCredentials = withCredentials
|
|
93
|
+
this._tokenParam = tokenParam
|
|
94
|
+
this._getToken = getToken
|
|
95
|
+
this._debug = debug
|
|
96
|
+
|
|
97
|
+
this._eventSource = null
|
|
98
|
+
this._reconnectTimer = null
|
|
99
|
+
this._connected = false
|
|
100
|
+
this._reconnecting = false
|
|
101
|
+
|
|
102
|
+
// Auto-connect or wait for auth:login
|
|
103
|
+
if (autoConnect) {
|
|
104
|
+
this.connect()
|
|
105
|
+
} else {
|
|
106
|
+
// Wait for auth:login signal to connect
|
|
107
|
+
this._signals.once('auth:login').then(() => {
|
|
108
|
+
this._log('Received auth:login, connecting SSE')
|
|
109
|
+
this.connect()
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Disconnect on auth:logout
|
|
114
|
+
this._signals.on('auth:logout', () => {
|
|
115
|
+
this._log('Received auth:logout, disconnecting SSE')
|
|
116
|
+
this.disconnect()
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build signal name with prefix
|
|
122
|
+
* @param {string} eventName - Event name
|
|
123
|
+
* @returns {string} Full signal name
|
|
124
|
+
*/
|
|
125
|
+
_buildSignal(eventName) {
|
|
126
|
+
return `${this._signalPrefix}:${eventName}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Debug logging
|
|
131
|
+
*/
|
|
132
|
+
_log(...args) {
|
|
133
|
+
if (this._debug) {
|
|
134
|
+
console.debug('[SSEBridge]', ...args)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build SSE URL with auth token
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
_buildUrl() {
|
|
143
|
+
const token = this._getToken?.()
|
|
144
|
+
const sseUrl = new URL(this._url, window.location.origin)
|
|
145
|
+
|
|
146
|
+
if (token && this._tokenParam) {
|
|
147
|
+
sseUrl.searchParams.set(this._tokenParam, token)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return sseUrl.toString()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Connect to SSE endpoint
|
|
155
|
+
*/
|
|
156
|
+
connect() {
|
|
157
|
+
// Clean up existing
|
|
158
|
+
if (this._eventSource) {
|
|
159
|
+
this._eventSource.close()
|
|
160
|
+
this._eventSource = null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Clear pending reconnect
|
|
164
|
+
if (this._reconnectTimer) {
|
|
165
|
+
clearTimeout(this._reconnectTimer)
|
|
166
|
+
this._reconnectTimer = null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const url = this._buildUrl()
|
|
171
|
+
this._log('Connecting to', url)
|
|
172
|
+
|
|
173
|
+
this._eventSource = new EventSource(url, {
|
|
174
|
+
withCredentials: this._withCredentials
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
this._eventSource.onopen = () => {
|
|
178
|
+
this._connected = true
|
|
179
|
+
this._reconnecting = false
|
|
180
|
+
this._log('Connected')
|
|
181
|
+
|
|
182
|
+
this._signals.emit(SSE_SIGNALS.CONNECTED, {
|
|
183
|
+
url: this._url,
|
|
184
|
+
timestamp: new Date()
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this._eventSource.onerror = (err) => {
|
|
189
|
+
this._connected = false
|
|
190
|
+
this._log('Connection error')
|
|
191
|
+
|
|
192
|
+
this._signals.emit(SSE_SIGNALS.ERROR, {
|
|
193
|
+
error: 'Connection error',
|
|
194
|
+
timestamp: new Date()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// Close broken connection
|
|
198
|
+
if (this._eventSource) {
|
|
199
|
+
this._eventSource.close()
|
|
200
|
+
this._eventSource = null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this._signals.emit(SSE_SIGNALS.DISCONNECTED, {
|
|
204
|
+
timestamp: new Date()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Schedule reconnect
|
|
208
|
+
this._scheduleReconnect()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle generic message events (event: message)
|
|
212
|
+
this._eventSource.onmessage = (event) => {
|
|
213
|
+
this._handleEvent('message', event)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// For named events, we need to add listeners dynamically
|
|
217
|
+
// Since EventSource doesn't expose event names, we use a wrapper approach:
|
|
218
|
+
// The server should send events with `event:` field, we handle them generically
|
|
219
|
+
|
|
220
|
+
// Note: Named events require explicit addEventListener.
|
|
221
|
+
// To support arbitrary event names, the app should either:
|
|
222
|
+
// 1. Use `message` event type and include event name in data
|
|
223
|
+
// 2. Pre-register known event names via registerEvents()
|
|
224
|
+
|
|
225
|
+
} catch (err) {
|
|
226
|
+
this._log('Connect error:', err.message)
|
|
227
|
+
this._signals.emit(SSE_SIGNALS.ERROR, {
|
|
228
|
+
error: err.message,
|
|
229
|
+
timestamp: new Date()
|
|
230
|
+
})
|
|
231
|
+
this._scheduleReconnect()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Register listeners for specific SSE event types
|
|
237
|
+
* Required because EventSource requires explicit addEventListener for named events.
|
|
238
|
+
*
|
|
239
|
+
* @param {string[]} eventNames - Event names to listen for
|
|
240
|
+
*/
|
|
241
|
+
registerEvents(eventNames) {
|
|
242
|
+
if (!this._eventSource) {
|
|
243
|
+
this._log('Cannot register events: not connected')
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const eventName of eventNames) {
|
|
248
|
+
this._eventSource.addEventListener(eventName, (event) => {
|
|
249
|
+
this._handleEvent(eventName, event)
|
|
250
|
+
})
|
|
251
|
+
this._log('Registered event:', eventName)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Handle incoming SSE event
|
|
257
|
+
* @param {string} eventName - Event name
|
|
258
|
+
* @param {MessageEvent} event - SSE event
|
|
259
|
+
*/
|
|
260
|
+
_handleEvent(eventName, event) {
|
|
261
|
+
let data
|
|
262
|
+
try {
|
|
263
|
+
data = JSON.parse(event.data)
|
|
264
|
+
} catch {
|
|
265
|
+
data = event.data
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const signal = this._buildSignal(eventName)
|
|
269
|
+
this._log(`Emitting ${signal}:`, data)
|
|
270
|
+
|
|
271
|
+
this._signals.emit(signal, {
|
|
272
|
+
event: eventName,
|
|
273
|
+
data,
|
|
274
|
+
timestamp: new Date(),
|
|
275
|
+
lastEventId: event.lastEventId
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Schedule reconnection
|
|
281
|
+
*/
|
|
282
|
+
_scheduleReconnect() {
|
|
283
|
+
if (this._reconnectDelay <= 0) return
|
|
284
|
+
if (this._reconnectTimer) return
|
|
285
|
+
|
|
286
|
+
this._reconnecting = true
|
|
287
|
+
this._log(`Reconnecting in ${this._reconnectDelay}ms`)
|
|
288
|
+
|
|
289
|
+
this._reconnectTimer = setTimeout(() => {
|
|
290
|
+
this._reconnectTimer = null
|
|
291
|
+
if (!this._connected) {
|
|
292
|
+
this.connect()
|
|
293
|
+
}
|
|
294
|
+
}, this._reconnectDelay)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Disconnect from SSE endpoint
|
|
299
|
+
*/
|
|
300
|
+
disconnect() {
|
|
301
|
+
if (this._reconnectTimer) {
|
|
302
|
+
clearTimeout(this._reconnectTimer)
|
|
303
|
+
this._reconnectTimer = null
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (this._eventSource) {
|
|
307
|
+
this._eventSource.close()
|
|
308
|
+
this._eventSource = null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this._connected) {
|
|
312
|
+
this._connected = false
|
|
313
|
+
this._signals.emit(SSE_SIGNALS.DISCONNECTED, {
|
|
314
|
+
timestamp: new Date()
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this._reconnecting = false
|
|
319
|
+
this._log('Disconnected')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Reconnect (disconnect + connect)
|
|
324
|
+
*/
|
|
325
|
+
reconnect() {
|
|
326
|
+
this.disconnect()
|
|
327
|
+
this.connect()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Check if connected
|
|
332
|
+
* @returns {boolean}
|
|
333
|
+
*/
|
|
334
|
+
isConnected() {
|
|
335
|
+
return this._connected
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check if reconnecting
|
|
340
|
+
* @returns {boolean}
|
|
341
|
+
*/
|
|
342
|
+
isReconnecting() {
|
|
343
|
+
return this._reconnecting
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Factory function to create SSEBridge
|
|
349
|
+
* @param {object} options - SSEBridge options
|
|
350
|
+
* @returns {SSEBridge}
|
|
351
|
+
*/
|
|
352
|
+
export function createSSEBridge(options) {
|
|
353
|
+
return new SSEBridge(options)
|
|
354
|
+
}
|
package/src/kernel/SignalBus.js
CHANGED
|
@@ -25,6 +25,14 @@ export const SIGNALS = {
|
|
|
25
25
|
ENTITY_UPDATED: 'entity:updated',
|
|
26
26
|
ENTITY_DELETED: 'entity:deleted',
|
|
27
27
|
|
|
28
|
+
// Auth lifecycle signals
|
|
29
|
+
AUTH_LOGIN: 'auth:login',
|
|
30
|
+
AUTH_LOGOUT: 'auth:logout',
|
|
31
|
+
AUTH_EXPIRED: 'auth:expired', // Emitted on 401/403 API responses
|
|
32
|
+
|
|
33
|
+
// API error signals
|
|
34
|
+
API_ERROR: 'api:error', // Emitted on any API error { status, message, url }
|
|
35
|
+
|
|
28
36
|
// Pattern for entity-specific signals
|
|
29
37
|
// Use buildSignal(entityName, action) for these
|
|
30
38
|
}
|
|
@@ -118,13 +126,8 @@ export class SignalBus {
|
|
|
118
126
|
* @returns {Promise<void>}
|
|
119
127
|
*/
|
|
120
128
|
async emitEntity(entityName, action, data) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Emit both specific and generic signals
|
|
125
|
-
// Specific first, then generic
|
|
126
|
-
await this.emit(specificSignal, { entity: entityName, data })
|
|
127
|
-
await this.emit(genericSignal, { entity: entityName, data })
|
|
129
|
+
const signal = buildSignal('entity', action)
|
|
130
|
+
await this.emit(signal, { entity: entityName, data })
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
/**
|
package/src/kernel/index.js
CHANGED
|
@@ -16,3 +16,22 @@ export {
|
|
|
16
16
|
EventRouter,
|
|
17
17
|
createEventRouter,
|
|
18
18
|
} from './EventRouter.js'
|
|
19
|
+
export {
|
|
20
|
+
SSEBridge,
|
|
21
|
+
createSSEBridge,
|
|
22
|
+
SSE_SIGNALS,
|
|
23
|
+
} from './SSEBridge.js'
|
|
24
|
+
export {
|
|
25
|
+
Module,
|
|
26
|
+
} from './Module.js'
|
|
27
|
+
export {
|
|
28
|
+
KernelContext,
|
|
29
|
+
createKernelContext,
|
|
30
|
+
} from './KernelContext.js'
|
|
31
|
+
export {
|
|
32
|
+
ModuleLoader,
|
|
33
|
+
createModuleLoader,
|
|
34
|
+
ModuleNotFoundError,
|
|
35
|
+
CircularDependencyError,
|
|
36
|
+
ModuleLoadError,
|
|
37
|
+
} from './ModuleLoader.js'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToastBridgeModule - Bridges signal bus to PrimeVue Toast
|
|
3
|
+
*
|
|
4
|
+
* This module integrates toast functionality via the signal bus:
|
|
5
|
+
* - Registers ToastListener component to handle toast:* signals
|
|
6
|
+
* - Provides useSignalToast() composable for emitting toasts
|
|
7
|
+
*
|
|
8
|
+
* Usage in modules:
|
|
9
|
+
* import { useSignalToast } from 'qdadm'
|
|
10
|
+
* const toast = useSignalToast()
|
|
11
|
+
* toast.success('Saved!', 'Your changes have been saved')
|
|
12
|
+
* toast.error('Error', 'Something went wrong')
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { createKernel, ToastBridgeModule } from 'qdadm'
|
|
16
|
+
*
|
|
17
|
+
* const kernel = createKernel()
|
|
18
|
+
* kernel.use(new ToastBridgeModule())
|
|
19
|
+
* await kernel.boot()
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Module } from '../kernel/Module.js'
|
|
23
|
+
import ToastListener from './ToastListener.vue'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Zone name for toast listener component
|
|
27
|
+
* Prefixed with _ to hide from ZonesCollector (internal zone)
|
|
28
|
+
*/
|
|
29
|
+
export const TOAST_ZONE = '_app:toasts'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ToastBridgeModule - Handles toast notifications via signals
|
|
33
|
+
*/
|
|
34
|
+
export class ToastBridgeModule extends Module {
|
|
35
|
+
/**
|
|
36
|
+
* Module identifier
|
|
37
|
+
* @type {string}
|
|
38
|
+
*/
|
|
39
|
+
static name = 'toast-bridge'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* No dependencies
|
|
43
|
+
* @type {string[]}
|
|
44
|
+
*/
|
|
45
|
+
static requires = []
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* High priority - should load early
|
|
49
|
+
* @type {number}
|
|
50
|
+
*/
|
|
51
|
+
static priority = 10
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Connect module to kernel
|
|
55
|
+
* @param {import('../kernel/KernelContext.js').KernelContext} ctx
|
|
56
|
+
*/
|
|
57
|
+
async connect(ctx) {
|
|
58
|
+
// Define the toast zone
|
|
59
|
+
ctx.zone(TOAST_ZONE)
|
|
60
|
+
|
|
61
|
+
// Register ToastListener component in the zone
|
|
62
|
+
ctx.block(TOAST_ZONE, {
|
|
63
|
+
id: 'toast-listener',
|
|
64
|
+
component: ToastListener,
|
|
65
|
+
weight: 0
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default ToastBridgeModule
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* ToastListener - Bridges signal bus to PrimeVue Toast
|
|
4
|
+
*
|
|
5
|
+
* Invisible component that listens to toast:* signals and displays
|
|
6
|
+
* them using PrimeVue's Toast component.
|
|
7
|
+
*
|
|
8
|
+
* This component should be registered in a zone via ToastBridgeModule.
|
|
9
|
+
*/
|
|
10
|
+
import { onMounted, onUnmounted, inject } from 'vue'
|
|
11
|
+
import { useToast } from 'primevue/usetoast'
|
|
12
|
+
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
const signals = inject('qdadmSignals')
|
|
15
|
+
|
|
16
|
+
let unsubscribe = null
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
if (!signals) {
|
|
20
|
+
console.warn('[ToastListener] No signals bus injected')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Listen to all toast signals
|
|
25
|
+
unsubscribe = signals.on('toast:*', (event) => {
|
|
26
|
+
const severity = event.name.split(':')[1] // toast:success -> success
|
|
27
|
+
toast.add({
|
|
28
|
+
severity,
|
|
29
|
+
summary: event.data?.summary,
|
|
30
|
+
detail: event.data?.detail,
|
|
31
|
+
life: event.data?.life ?? 3000
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
onUnmounted(() => {
|
|
37
|
+
if (unsubscribe) {
|
|
38
|
+
unsubscribe()
|
|
39
|
+
unsubscribe = null
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<!-- Invisible listener component -->
|
|
46
|
+
<span style="display: none" />
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast Module - Signal-based toast notifications
|
|
3
|
+
*
|
|
4
|
+
* Provides toast notifications that go through the signal bus,
|
|
5
|
+
* allowing for proper debugging and interception.
|
|
6
|
+
*
|
|
7
|
+
* Components:
|
|
8
|
+
* - ToastBridgeModule: Registers ToastListener for handling signals
|
|
9
|
+
* - ToastListener: Vue component that displays toasts via PrimeVue
|
|
10
|
+
* - useSignalToast: Composable for emitting toast signals
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { ToastBridgeModule, TOAST_ZONE } from './ToastBridgeModule.js'
|
|
14
|
+
export { useSignalToast } from './useSignalToast.js'
|
|
15
|
+
export { default as ToastListener } from './ToastListener.vue'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSignalToast - Composable for emitting toast notifications via signals
|
|
3
|
+
*
|
|
4
|
+
* This composable provides a simple API for showing toasts that go through
|
|
5
|
+
* the signal bus. Works with ToastBridgeModule which handles displaying
|
|
6
|
+
* the toasts via PrimeVue.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { useSignalToast } from 'qdadm'
|
|
10
|
+
*
|
|
11
|
+
* // With explicit emitter name
|
|
12
|
+
* const toast = useSignalToast('MyComponent')
|
|
13
|
+
* toast.success('Saved!', 'Your changes have been saved')
|
|
14
|
+
*
|
|
15
|
+
* // Without emitter (defaults to 'unknown')
|
|
16
|
+
* const toast = useSignalToast()
|
|
17
|
+
* toast.error('Error', 'Something went wrong')
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { inject, getCurrentInstance } from 'vue'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Composable for emitting toast notifications via signal bus
|
|
24
|
+
* @param {string} [emitter] - Identifier for the toast source (component/module name)
|
|
25
|
+
* @returns {object} Toast API
|
|
26
|
+
*/
|
|
27
|
+
export function useSignalToast(emitter) {
|
|
28
|
+
const signals = inject('qdadmSignals')
|
|
29
|
+
|
|
30
|
+
// Try to auto-detect emitter from current component if not provided
|
|
31
|
+
const instance = getCurrentInstance()
|
|
32
|
+
const resolvedEmitter =
|
|
33
|
+
emitter || instance?.type?.name || instance?.type?.__name || 'unknown'
|
|
34
|
+
|
|
35
|
+
if (!signals) {
|
|
36
|
+
console.warn('[useSignalToast] No signals bus injected - toasts will not work')
|
|
37
|
+
// Return no-op functions
|
|
38
|
+
return {
|
|
39
|
+
success: () => {},
|
|
40
|
+
error: () => {},
|
|
41
|
+
info: () => {},
|
|
42
|
+
warn: () => {},
|
|
43
|
+
add: () => {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Show a toast notification
|
|
49
|
+
* @param {string} severity - Toast severity (success, info, warn, error)
|
|
50
|
+
* @param {string} summary - Toast title
|
|
51
|
+
* @param {string} [detail] - Toast detail message
|
|
52
|
+
* @param {number} [life=3000] - Duration in ms (0 for sticky)
|
|
53
|
+
*/
|
|
54
|
+
function add(severity, summary, detail, life = 3000) {
|
|
55
|
+
signals.emit(`toast:${severity}`, { summary, detail, life, emitter: resolvedEmitter })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
/**
|
|
60
|
+
* Show success toast
|
|
61
|
+
* @param {string} summary - Toast title
|
|
62
|
+
* @param {string} [detail] - Toast detail message
|
|
63
|
+
* @param {number} [life=3000] - Duration in ms
|
|
64
|
+
*/
|
|
65
|
+
success(summary, detail, life = 3000) {
|
|
66
|
+
add('success', summary, detail, life)
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Show error toast
|
|
71
|
+
* @param {string} summary - Toast title
|
|
72
|
+
* @param {string} [detail] - Toast detail message
|
|
73
|
+
* @param {number} [life=5000] - Duration in ms (longer for errors)
|
|
74
|
+
*/
|
|
75
|
+
error(summary, detail, life = 5000) {
|
|
76
|
+
add('error', summary, detail, life)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Show info toast
|
|
81
|
+
* @param {string} summary - Toast title
|
|
82
|
+
* @param {string} [detail] - Toast detail message
|
|
83
|
+
* @param {number} [life=3000] - Duration in ms
|
|
84
|
+
*/
|
|
85
|
+
info(summary, detail, life = 3000) {
|
|
86
|
+
add('info', summary, detail, life)
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Show warning toast
|
|
91
|
+
* @param {string} summary - Toast title
|
|
92
|
+
* @param {string} [detail] - Toast detail message
|
|
93
|
+
* @param {number} [life=4000] - Duration in ms
|
|
94
|
+
*/
|
|
95
|
+
warn(summary, detail, life = 4000) {
|
|
96
|
+
add('warn', summary, detail, life)
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generic add method (compatible with PrimeVue toast.add)
|
|
101
|
+
* @param {object} options - Toast options
|
|
102
|
+
* @param {string} options.severity - Toast severity
|
|
103
|
+
* @param {string} options.summary - Toast title
|
|
104
|
+
* @param {string} [options.detail] - Toast detail
|
|
105
|
+
* @param {number} [options.life] - Duration in ms
|
|
106
|
+
*/
|
|
107
|
+
add(options) {
|
|
108
|
+
add(options.severity || 'info', options.summary, options.detail, options.life)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default useSignalToast
|