@innertia-solutions/innertia-nuxt 0.1.36 → 0.1.38
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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes to `@innertia-solutions/innertia-nuxt` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `Kanban.Standard`: en modo `bordered=false`, el `ring` de highlight al arrastrar sobre una columna se recortaba por arriba (el contenedor `overflow-x-auto` sin padding clippeaba los 2px del ring). Se agrega `p-1` al scroll container en ese modo.
|
|
10
|
+
|
|
7
11
|
### BREAKING CHANGES
|
|
8
12
|
|
|
9
13
|
#### apps → contexts rename
|
|
@@ -69,11 +69,11 @@ const props = defineProps({
|
|
|
69
69
|
filters: { type: Array, default: () => [] },
|
|
70
70
|
showFilters: { type: Boolean, default: true },
|
|
71
71
|
|
|
72
|
-
// Realtime (solo endpoint mode):
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
// Realtime (solo endpoint mode): por defecto auto-suscribe a los canales que
|
|
73
|
+
// el backend declara en meta.channels. `realtimeChannel` es un override legacy
|
|
74
|
+
// que agrega un canal explícito a la lista.
|
|
75
|
+
realtime: { type: Boolean, default: true },
|
|
76
|
+
realtimeChannel: { type: String, default: null },
|
|
77
77
|
|
|
78
78
|
// Caché en sessionStorage
|
|
79
79
|
cached: { type: Boolean, default: false },
|
|
@@ -368,6 +368,7 @@ const fetchAll = async () => {
|
|
|
368
368
|
const res = await api.post(props.endpoint, buildBody())
|
|
369
369
|
rows.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : [])
|
|
370
370
|
meta.value = res?.meta ?? null
|
|
371
|
+
syncRealtime()
|
|
371
372
|
saveToCache()
|
|
372
373
|
emit('loaded', res)
|
|
373
374
|
} catch (e) {
|
|
@@ -409,6 +410,7 @@ const loadFromCache = () => {
|
|
|
409
410
|
search.value = cached.search ?? ''
|
|
410
411
|
activeFilters.value = cached.filters ?? {}
|
|
411
412
|
isDataFromCache.value = true
|
|
413
|
+
syncRealtime()
|
|
412
414
|
return true
|
|
413
415
|
} catch { return false }
|
|
414
416
|
}
|
|
@@ -535,19 +537,20 @@ const onDocMousedown = (e) => {
|
|
|
535
537
|
}
|
|
536
538
|
|
|
537
539
|
// ─── Realtime ────────────────────────────────────────────────────────────────
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
540
|
+
// Auto-suscribe a los canales que el backend declara en meta.channels (más el
|
|
541
|
+
// override legacy `realtimeChannel`, si se pasa). Solo en endpoint mode.
|
|
542
|
+
const tableRealtime = (props.realtime && !isItemsMode.value) ? useTableRealtime(() => fetchAll()) : null
|
|
543
|
+
function syncRealtime() {
|
|
544
|
+
if (!tableRealtime) return
|
|
545
|
+
const fromMeta = Array.isArray(meta.value?.channels) ? meta.value.channels : []
|
|
546
|
+
const channels = props.realtimeChannel ? [...new Set([...fromMeta, props.realtimeChannel])] : fromMeta
|
|
547
|
+
tableRealtime.sync(channels)
|
|
544
548
|
}
|
|
545
549
|
|
|
546
550
|
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
547
551
|
onMounted(() => {
|
|
548
552
|
window.addEventListener('keydown', onEsc)
|
|
549
553
|
document.addEventListener('mousedown', onDocMousedown)
|
|
550
|
-
setupRealtime()
|
|
551
554
|
if (isItemsMode.value) return // rows ya cargados por el watcher immediate
|
|
552
555
|
if (loadFromCache()) return
|
|
553
556
|
fetchAll()
|
|
@@ -556,7 +559,7 @@ onMounted(() => {
|
|
|
556
559
|
onBeforeUnmount(() => {
|
|
557
560
|
window.removeEventListener('keydown', onEsc)
|
|
558
561
|
document.removeEventListener('mousedown', onDocMousedown)
|
|
559
|
-
|
|
562
|
+
tableRealtime?.teardown()
|
|
560
563
|
})
|
|
561
564
|
|
|
562
565
|
defineExpose({ reload: fetchAll, rows })
|
|
@@ -692,7 +695,7 @@ defineExpose({ reload: fetchAll, rows })
|
|
|
692
695
|
]"
|
|
693
696
|
>
|
|
694
697
|
<div
|
|
695
|
-
:class="['overflow-x-auto', bordered ? 'p-3' : '']"
|
|
698
|
+
:class="['overflow-x-auto', bordered ? 'p-3' : 'p-1']"
|
|
696
699
|
:style="sizeStyle"
|
|
697
700
|
>
|
|
698
701
|
<div v-if="loading && !rows.length" class="flex items-center justify-center py-16 gap-2 text-muted-foreground">
|
|
@@ -15,10 +15,11 @@ const props = defineProps({
|
|
|
15
15
|
searchPlaceholder: { type: String, default: 'Buscar...' },
|
|
16
16
|
showSearch: { type: Boolean, default: true },
|
|
17
17
|
showFilters: { type: Boolean, default: true },
|
|
18
|
-
// Realtime:
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
// Realtime: auto-suscripción a los canales que el backend expone en meta.channels
|
|
19
|
+
// (`entity.{tabla}.changed` → reload()). realtimeChannel queda como override manual
|
|
20
|
+
// (legacy, se suma a los del backend); realtime=false apaga la auto-suscripción.
|
|
21
|
+
realtimeChannel: { type: String, default: null }, // override manual (legacy)
|
|
22
|
+
realtime: { type: Boolean, default: true }, // apagar auto-suscripción
|
|
22
23
|
showExport: { type: Boolean, default: true },
|
|
23
24
|
showColumns: { type: Boolean, default: true },
|
|
24
25
|
/** Mostrar selector "Filas por página" en el footer. Default false. */
|
|
@@ -382,10 +383,22 @@ const handleRowClick = (row) => {
|
|
|
382
383
|
previewRow.value = row
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
// ─── Realtime ────────────────────────────────────────────────────────────────
|
|
387
|
+
// Auto-suscripción a los canales que el backend expone en meta.channels. El
|
|
388
|
+
// override manual (realtimeChannel) se suma a los del backend. realtime=false
|
|
389
|
+
// desactiva la suscripción.
|
|
390
|
+
const tableRealtime = props.realtime ? useTableRealtime(() => reload()) : null
|
|
391
|
+
|
|
392
|
+
function channelsFrom(meta) {
|
|
393
|
+
const fromMeta = Array.isArray(meta?.channels) ? meta.channels : []
|
|
394
|
+
return props.realtimeChannel ? [...new Set([...fromMeta, props.realtimeChannel])] : fromMeta
|
|
395
|
+
}
|
|
396
|
+
|
|
385
397
|
// Cuando recarga la data, mantener viva la fila previewada (o cerrar si fue eliminada).
|
|
386
398
|
const handleLoaded = (res) => {
|
|
387
399
|
emit('loaded', res)
|
|
388
400
|
if (res?.meta) tableMeta.value = res.meta
|
|
401
|
+
tableRealtime?.sync(channelsFrom(res?.meta))
|
|
389
402
|
if (previewRow.value && Array.isArray(res?.data)) {
|
|
390
403
|
const fresh = res.data.find(r => r.id === previewRow.value.id)
|
|
391
404
|
if (fresh) previewRow.value = fresh
|
|
@@ -415,20 +428,10 @@ onMounted(() => {
|
|
|
415
428
|
})
|
|
416
429
|
})
|
|
417
430
|
|
|
418
|
-
// Realtime opcional: recarga (preservando UI) al recibir eventos del canal.
|
|
419
|
-
const realtime = props.realtimeChannel ? useRealtime() : null
|
|
420
|
-
async function setupRealtime() {
|
|
421
|
-
if (!realtime) return
|
|
422
|
-
await realtime.connect()
|
|
423
|
-
const events = props.realtimeEvents ?? [`${props.realtimeChannel}.updated`]
|
|
424
|
-
realtime.subscribe(props.realtimeChannel, Object.fromEntries(events.map(e => [e, () => reload()])))
|
|
425
|
-
}
|
|
426
|
-
|
|
427
431
|
onMounted(async () => {
|
|
428
432
|
previewEnabled.value = !!slots.preview
|
|
429
433
|
window.addEventListener('keydown', onEsc)
|
|
430
434
|
document.addEventListener('mousedown', onDocMousedown)
|
|
431
|
-
setupRealtime()
|
|
432
435
|
|
|
433
436
|
// Load column preferences from backend
|
|
434
437
|
if (props.persistPreferences && resolvedName.value) {
|
|
@@ -448,7 +451,7 @@ onMounted(async () => {
|
|
|
448
451
|
onBeforeUnmount(() => {
|
|
449
452
|
window.removeEventListener('keydown', onEsc)
|
|
450
453
|
document.removeEventListener('mousedown', onDocMousedown)
|
|
451
|
-
|
|
454
|
+
tableRealtime?.teardown()
|
|
452
455
|
})
|
|
453
456
|
|
|
454
457
|
// ─── Column panel ─────────────────────────────────────────────────────────────
|
|
@@ -78,6 +78,10 @@ const props = defineProps({
|
|
|
78
78
|
|
|
79
79
|
// Resalta la fila seleccionada (id del nodo). Para selección externa via @row-click.
|
|
80
80
|
selectedId: { type: [String, Number], default: null },
|
|
81
|
+
|
|
82
|
+
// Realtime: auto-suscripción a los canales que el backend declara en meta.channels
|
|
83
|
+
// (`entity.{tabla}.changed` → reload() preservando expansión). realtime=false apaga.
|
|
84
|
+
realtime: { type: Boolean, default: true },
|
|
81
85
|
})
|
|
82
86
|
|
|
83
87
|
const sizeStyle = computed(() => {
|
|
@@ -202,6 +206,7 @@ const fetchInitial = async () => {
|
|
|
202
206
|
const res = await api.post(props.endpoint, buildBody())
|
|
203
207
|
roots.value = res?.data ?? []
|
|
204
208
|
meta.value = res?.meta ?? null
|
|
209
|
+
syncRealtime()
|
|
205
210
|
childrenById.value = {}
|
|
206
211
|
// Colapsar todo: sin esto, un nodo expandido antes de cambiar search/params
|
|
207
212
|
// queda con el chevron abierto pero sin hijos (childrenById se vació).
|
|
@@ -252,6 +257,15 @@ const reload = async () => {
|
|
|
252
257
|
}
|
|
253
258
|
}
|
|
254
259
|
|
|
260
|
+
// ─── Realtime ─────────────────────────────────────────────────────────────────
|
|
261
|
+
// Auto-suscripción a los canales del backend (meta.channels). Cada `{channel}.changed`
|
|
262
|
+
// dispara reload() (debounced), refrescando datos sin perder la expansión actual.
|
|
263
|
+
const tableRealtime = props.realtime ? useTableRealtime(() => reload()) : null
|
|
264
|
+
function syncRealtime() {
|
|
265
|
+
if (!tableRealtime) return
|
|
266
|
+
tableRealtime.sync(Array.isArray(meta.value?.channels) ? meta.value.channels : [])
|
|
267
|
+
}
|
|
268
|
+
|
|
255
269
|
// ─── Cache ────────────────────────────────────────────────────────────────────
|
|
256
270
|
const cacheKey = computed(() => {
|
|
257
271
|
if (!props.cached || !props.name) return null
|
|
@@ -296,6 +310,7 @@ const loadFromCacheOnMount = () => {
|
|
|
296
310
|
activeFilters.value = cached.filters ?? {}
|
|
297
311
|
columnVisibility.value = cached.columnVisibility ?? columnVisibility.value
|
|
298
312
|
isDataFromCache.value = true
|
|
313
|
+
syncRealtime()
|
|
299
314
|
return true
|
|
300
315
|
} catch {
|
|
301
316
|
return false
|
|
@@ -567,6 +582,7 @@ onMounted(() => {
|
|
|
567
582
|
onBeforeUnmount(() => {
|
|
568
583
|
window.removeEventListener('keydown', onEsc)
|
|
569
584
|
document.removeEventListener('mousedown', onDocMousedown)
|
|
585
|
+
tableRealtime?.teardown()
|
|
570
586
|
})
|
|
571
587
|
|
|
572
588
|
defineExpose({
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suscribe un componente de datos (table/kanban/tree) a uno o varios canales
|
|
3
|
+
* `entity.{tabla}` y dispara reloadFn() debounceado al recibir `{canal}.changed`.
|
|
4
|
+
*
|
|
5
|
+
* - sync(channels): difunde la lista de canales (de meta.channels). Suscribe los
|
|
6
|
+
* nuevos, des-suscribe los que ya no están. Idempotente: llamar tras cada fetch.
|
|
7
|
+
* - teardown(): des-suscribe todo (llamar en onBeforeUnmount).
|
|
8
|
+
*
|
|
9
|
+
* Coalesce: varios pings en la ventana de debounce → un solo reload.
|
|
10
|
+
* In-flight guard: si llega un ping mientras reloadFn está corriendo, re-dispara al terminar.
|
|
11
|
+
*/
|
|
12
|
+
export function useTableRealtime(reloadFn, { debounceMs = 400 } = {}) {
|
|
13
|
+
const rt = useRealtime()
|
|
14
|
+
const subscribed = new Set()
|
|
15
|
+
let timer = null
|
|
16
|
+
let inFlight = false
|
|
17
|
+
let pending = false
|
|
18
|
+
|
|
19
|
+
const fire = async () => {
|
|
20
|
+
inFlight = true
|
|
21
|
+
try { await reloadFn() }
|
|
22
|
+
finally {
|
|
23
|
+
inFlight = false
|
|
24
|
+
if (pending) { pending = false; ping() }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ping = () => {
|
|
29
|
+
if (inFlight) { pending = true; return }
|
|
30
|
+
clearTimeout(timer)
|
|
31
|
+
timer = setTimeout(fire, debounceMs)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sync = async (channels = []) => {
|
|
35
|
+
const next = new Set(Array.isArray(channels) ? channels.filter(Boolean) : [])
|
|
36
|
+
if (!next.size && !subscribed.size) return
|
|
37
|
+
if (next.size) await rt.connect()
|
|
38
|
+
|
|
39
|
+
for (const ch of [...subscribed]) {
|
|
40
|
+
if (!next.has(ch)) { rt.unsubscribe(ch); subscribed.delete(ch) }
|
|
41
|
+
}
|
|
42
|
+
for (const ch of next) {
|
|
43
|
+
if (!subscribed.has(ch)) {
|
|
44
|
+
rt.subscribe(ch, { [`${ch}.changed`]: ping })
|
|
45
|
+
subscribed.add(ch)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const teardown = () => {
|
|
51
|
+
clearTimeout(timer)
|
|
52
|
+
for (const ch of [...subscribed]) rt.unsubscribe(ch)
|
|
53
|
+
subscribed.clear()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { sync, teardown }
|
|
57
|
+
}
|