@quazardous/qdadm 2.29.0 → 2.30.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "2.29.0",
3
+ "version": "2.30.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -58,7 +58,8 @@ import {
58
58
  type ShowResolvedFieldConfig,
59
59
  } from './createShowFieldResolver'
60
60
  import type { OrchestratorLike } from '../entity/EntityManager.interface'
61
- import type { ButtonSeverity } from '../types'
61
+ import type { LiveEntityPolicy } from '../entity/EntityManager.types'
62
+ import type { ButtonSeverity, EntityRecord } from '../types'
62
63
  import { useActionRegistry } from './useActionRegistry'
63
64
  import { useLiveEntity } from './useLiveEntity'
64
65
 
@@ -329,17 +330,35 @@ export function useEntityItemShowPage<T = Record<string, unknown>>(
329
330
  const notifications = useNotifications()
330
331
  const route = useRoute()
331
332
  useLiveEntity(entity, manager, async () => {
333
+ // A burst of events is one reload, so this is the record from before the burst.
334
+ const before = data.value
332
335
  const record = await base.reload()
333
336
  // A change made elsewhere to the record on screen leaves a line in the
334
- // history (#2677): short-lived, linked back to it, never a pop-up.
337
+ // history (#2677): short-lived, linked back to it, never a pop-up. The
338
+ // entity may say the change is not worth one, or what it means (#2685).
335
339
  if (!record) return
336
- const name = entityLabel.value ? `"${entityLabel.value}"` : `#${entityId.value}`
337
- notifications.addNotification({
338
- severity: 'info',
339
- summary: `${manager.label ?? entity} ${name} updated elsewhere`,
340
- keep: 'short',
341
- ...(route?.name ? { to: { name: String(route.name), params: { ...route.params } } } : {}),
342
- })
340
+ const to = route?.name ? { to: { name: String(route.name), params: { ...route.params } } } : {}
341
+ const describeUpdate = (manager as { live?: LiveEntityPolicy }).live?.describeUpdate
342
+ if (!describeUpdate) {
343
+ const name = entityLabel.value ? `"${entityLabel.value}"` : `#${entityId.value}`
344
+ notifications.addNotification({
345
+ severity: 'info',
346
+ summary: `${manager.label ?? entity} ${name} updated elsewhere`,
347
+ keep: 'short',
348
+ ...to,
349
+ })
350
+ return
351
+ }
352
+ if (!before) return
353
+ let entry
354
+ try {
355
+ entry = describeUpdate(before as unknown as EntityRecord, record as unknown as EntityRecord)
356
+ } catch (err) {
357
+ console.warn(`[qdadm] live.describeUpdate failed for ${entity} #${entityId.value}:`, err)
358
+ return
359
+ }
360
+ if (!entry) return
361
+ notifications.addNotification({ severity: 'info', keep: 'short', ...to, ...entry })
343
362
  }, {
344
363
  id: () => entityId.value,
345
364
  // The update has arrived: say so on the badge at once — before the reload is
@@ -105,7 +105,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
105
105
  protected _warmup: boolean
106
106
  protected _authSensitive: boolean
107
107
  /** Reaction policy for changes made outside this session (#1888). */
108
- protected _live: Required<LiveEntityPolicy>
108
+ protected _live: LiveEntityPolicy & Required<Pick<LiveEntityPolicy, 'refresh' | 'coalesceMs'>>
109
109
  protected _system: boolean
110
110
 
111
111
  protected _scopeWhitelist: string[] | null
@@ -215,6 +215,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
215
215
  this._live = {
216
216
  refresh: live?.refresh ?? 'mounted',
217
217
  coalesceMs: live?.coalesceMs ?? 300,
218
+ ...(live?.describeUpdate ? { describeUpdate: live.describeUpdate } : {}),
218
219
  }
219
220
  this._system = system
220
221
 
@@ -640,7 +641,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
640
641
  * Check if user can read entities
641
642
  */
642
643
  /** Reaction policy for changes made outside this session (#1888). */
643
- get live(): Required<LiveEntityPolicy> {
644
+ get live(): LiveEntityPolicy & Required<Pick<LiveEntityPolicy, 'refresh' | 'coalesceMs'>> {
644
645
  return this._live
645
646
  }
646
647
 
@@ -17,6 +17,7 @@ import type { HookRegistry } from '../hooks/HookRegistry'
17
17
  // Circular type import — safe because `import type` is erased at runtime
18
18
  import type { EntityManager } from './EntityManager'
19
19
  import type { RouteStatePersister } from '../routeState'
20
+ import type { NewNotification } from '../notifications/NotificationStore'
20
21
 
21
22
  // ============ INTERNAL TYPES ============
22
23
 
@@ -230,8 +231,24 @@ export interface LiveEntityPolicy {
230
231
  * Default 300. `0` reloads on every event — rarely what you want.
231
232
  */
232
233
  coalesceMs?: number
234
+ /**
235
+ * Whether a live reload of a detail page is worth a notification entry, and
236
+ * what it says (#2685). Called once per reload, with the record that was on
237
+ * screen and the reloaded one.
238
+ *
239
+ * Return `null` / `undefined` for no entry (the badge still flashes), or the
240
+ * entry: `severity` defaults to `'info'`, `keep` to `'short'`, `to` to the
241
+ * record's page. Without this hook, the page adds a generic
242
+ * "… updated elsewhere" entry. If it throws, no entry is added.
243
+ *
244
+ * Do not mutate `before` or `after`: they are the page's own records.
245
+ */
246
+ describeUpdate?: (before: EntityRecord, after: EntityRecord) => LiveUpdateEntry | null | undefined
233
247
  }
234
248
 
249
+ /** The notification entry `LiveEntityPolicy.describeUpdate` asks for (#2685). */
250
+ export type LiveUpdateEntry = Omit<NewNotification, 'severity'> & Partial<Pick<NewNotification, 'severity'>>
251
+
235
252
  /**
236
253
  * EntityManager constructor options
237
254
  */
package/src/index.ts CHANGED
@@ -41,7 +41,7 @@ export { SSEBridge, createSSEBridge, SSE_SIGNALS } from './kernel/SSEBridge'
41
41
  export type { SSEBridgeOptions } from './kernel/SSEBridge'
42
42
  export { useLiveEntity } from './composables/useLiveEntity'
43
43
  export type { UseLiveEntityOptions } from './composables/useLiveEntity'
44
- export type { LiveEntityPolicy } from './entity/EntityManager.types'
44
+ export type { LiveEntityPolicy, LiveUpdateEntry } from './entity/EntityManager.types'
45
45
  export { LiveEntityRouter, createLiveEntityRouter } from './kernel/LiveEntityRouter'
46
46
  export type {
47
47
  LiveEntityRouterOptions,
@@ -8,7 +8,7 @@
8
8
  * - Mobile: Full width overlay at bottom of screen
9
9
  *
10
10
  * Features:
11
- * - Inline action bar (mark read, clear, close) at top right
11
+ * - Toolbar row (mark read, clear, close) above the list
12
12
  * - Status items section (custom module items)
13
13
  * - Notification list (most recent first)
14
14
  * - Empty state
@@ -189,16 +189,15 @@ function formatTime(timestamp: number): string {
189
189
  left: calc(var(--fad-sidebar-width-collapsed, 2.5rem) + 0.375rem);
190
190
  }
191
191
 
192
- /* Inline toolbar - floats top-right inside the panel */
192
+ /* Toolbar - its own row above the list, so its buttons never cover an entry's dismiss button */
193
193
  .notification-toolbar {
194
- position: absolute;
195
- top: 0;
196
- right: 0;
197
194
  display: flex;
198
195
  align-items: center;
196
+ justify-content: flex-end;
199
197
  gap: 1px;
200
198
  padding: 0.25rem;
201
- z-index: 1;
199
+ border-bottom: 1px solid var(--p-surface-100, #f1f5f9);
200
+ flex-shrink: 0;
202
201
  }
203
202
 
204
203
  .notification-toolbar-btn {
@@ -228,7 +227,6 @@ function formatTime(timestamp: number): string {
228
227
  /* Status items */
229
228
  .notification-panel-status {
230
229
  padding: 0.5rem 0.625rem;
231
- padding-right: 2.5rem;
232
230
  border-bottom: 1px solid var(--p-surface-100, #f1f5f9);
233
231
  flex-shrink: 0;
234
232
  }
@@ -309,10 +307,6 @@ function formatTime(timestamp: number): string {
309
307
  transition: background 0.1s;
310
308
  }
311
309
 
312
- .notification-item:first-child {
313
- padding-right: 2.5rem;
314
- }
315
-
316
310
  .notification-item:hover {
317
311
  background: var(--p-surface-50, #f8fafc);
318
312
  }
@@ -175,7 +175,7 @@ export interface EntityManagerOptions<T extends EntityRecord = EntityRecord> {
175
175
  warmup?: boolean
176
176
  authSensitive?: boolean
177
177
  /** What this entity does when it changed elsewhere (#1888). */
178
- live?: { refresh?: 'mounted' | false; coalesceMs?: number }
178
+ live?: import('../entity/EntityManager.types').LiveEntityPolicy
179
179
  system?: boolean
180
180
  scopeWhitelist?: string[] | null
181
181
  isOwn?: ((record: T, user: unknown) => boolean) | null