@innertia-solutions/innertia-nuxt 0.1.38 → 0.1.40
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 +5 -0
- package/components/Forms/Select.vue +8 -21
- package/components/Kanban/Standard.vue +93 -23
- package/package.json +2 -1
- package/plugins/preline.client.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,13 @@ All notable changes to `@innertia-solutions/innertia-nuxt` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Kanban.Standard`: reordenamiento intra-columna con drag&drop (prop `reorderEndpoint`); orden por `position`, placeholder de inserción, y persistencia vía POST. Retrocompatible: sin la prop se comporta igual que antes (solo cross-column).
|
|
10
|
+
|
|
7
11
|
### Fixed
|
|
8
12
|
|
|
13
|
+
- `Forms.Select`: el dropdown ahora usa `dropdownScope:'window'` (Preline lo teletransporta al `<body>` y lo posiciona con floating-ui). Deja de recortarse dentro de contenedores con scroll —modales, paneles— y flota por encima (`z-[10000]`). Se expone `window.FloatingUIDOM` en el plugin de Preline y se agrega `@floating-ui/dom` como dependencia.
|
|
9
14
|
- `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
15
|
|
|
11
16
|
### BREAKING CHANGES
|
|
@@ -111,9 +111,10 @@ const handleChange = (e: Event) => {
|
|
|
111
111
|
@change="handleChange"
|
|
112
112
|
data-hs-select='{
|
|
113
113
|
"placeholder": "Seleccionar...",
|
|
114
|
+
"dropdownScope": "window",
|
|
114
115
|
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
|
115
|
-
"toggleClasses": "innertia-field hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50
|
|
116
|
-
"dropdownClasses": "
|
|
116
|
+
"toggleClasses": "innertia-field hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative pe-9 flex gap-x-2 items-center text-nowrap cursor-pointer text-start",
|
|
117
|
+
"dropdownClasses": "z-[10000] w-full max-h-72 py-1 bg-[color:var(--field-dropdown-bg)] border border-[color:var(--field-border)] rounded-control overflow-hidden overflow-y-auto shadow-lg",
|
|
117
118
|
"optionClasses": "py-2 px-3 w-full text-sm text-foreground cursor-pointer bg-transparent hover:bg-muted-hover focus:outline-hidden focus:bg-muted-hover",
|
|
118
119
|
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
|
119
120
|
}'
|
|
@@ -141,24 +142,10 @@ const handleChange = (e: Event) => {
|
|
|
141
142
|
|
|
142
143
|
<style>
|
|
143
144
|
/*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* del componente Vue (button generado, no del template).
|
|
145
|
+
* Con `dropdownScope:'window'` Preline teletransporta el panel del dropdown al
|
|
146
|
+
* <body> y lo posiciona con floating-ui (fixed + offset). Así escapa el overflow
|
|
147
|
+
* de cualquier contenedor con scroll (modales, paneles, etc.) y flota por encima.
|
|
148
|
+
* El trigger conserva sus bordes/esquinas completos porque el panel ya no se
|
|
149
|
+
* "fusiona" con él — flota separado.
|
|
150
150
|
*/
|
|
151
|
-
.hs-select [aria-expanded="true"].innertia-field {
|
|
152
|
-
border-bottom-left-radius: 0 !important;
|
|
153
|
-
border-bottom-right-radius: 0 !important;
|
|
154
|
-
border-bottom-color: transparent !important;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/* Pegar el dropdown al trigger eliminando cualquier offset/margen que
|
|
158
|
-
* Preline pueda inyectar. Target amplio: cualquier div con la clase
|
|
159
|
-
* `rounded-b-control` dentro del wrapper `.hs-select`. */
|
|
160
|
-
.hs-select div[class*="rounded-b-control"] {
|
|
161
|
-
top: 100% !important;
|
|
162
|
-
margin-top: -1px !important;
|
|
163
|
-
}
|
|
164
151
|
</style>
|
|
@@ -56,6 +56,9 @@ const props = defineProps({
|
|
|
56
56
|
states: { type: Array, required: true },
|
|
57
57
|
moveMutation: { type: Function, default: null },
|
|
58
58
|
|
|
59
|
+
// Endpoint para persistir reorden (POST). Si null → solo cross-column, sin placeholder.
|
|
60
|
+
reorderEndpoint: { type: String, default: null },
|
|
61
|
+
|
|
59
62
|
// Toolbar visibility
|
|
60
63
|
showSearch: { type: Boolean, default: true },
|
|
61
64
|
searchPlaceholder:{ type: String, default: 'Buscar...' },
|
|
@@ -355,6 +358,7 @@ const buildBody = () => ({
|
|
|
355
358
|
per_page: props.perPage,
|
|
356
359
|
search: search.value.trim(),
|
|
357
360
|
...(enrichedFilters.value.length ? { filters: enrichedFilters.value } : {}),
|
|
361
|
+
...(props.reorderEndpoint ? { board: true } : {}),
|
|
358
362
|
})
|
|
359
363
|
|
|
360
364
|
const fetchAll = async () => {
|
|
@@ -433,6 +437,11 @@ const columnRows = computed(() => {
|
|
|
433
437
|
const state = row[props.stateKey]
|
|
434
438
|
if (map[state]) map[state].push(row)
|
|
435
439
|
}
|
|
440
|
+
if (props.reorderEndpoint) {
|
|
441
|
+
for (const k of Object.keys(map)) {
|
|
442
|
+
map[k].sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
443
|
+
}
|
|
444
|
+
}
|
|
436
445
|
return map
|
|
437
446
|
})
|
|
438
447
|
|
|
@@ -442,21 +451,73 @@ const totalRendered = computed(() => rows.value.length)
|
|
|
442
451
|
const draggedId = ref(null)
|
|
443
452
|
const draggedFrom = ref(null)
|
|
444
453
|
const dragOverState = ref(null)
|
|
454
|
+
const dragOverIndex = ref(null) // índice de inserción (placeholder) en la columna dragOverState
|
|
445
455
|
|
|
446
456
|
const onDragStart = (row, state) => {
|
|
447
457
|
draggedId.value = row.id
|
|
448
458
|
draggedFrom.value = state
|
|
449
459
|
}
|
|
450
|
-
const onDragOver
|
|
451
|
-
|
|
460
|
+
const onDragOver = (e, state) => {
|
|
461
|
+
e.preventDefault()
|
|
462
|
+
dragOverState.value = state
|
|
463
|
+
if (!props.reorderEndpoint) return
|
|
464
|
+
if (dragOverIndex.value === null) dragOverIndex.value = columnRows.value[state]?.length ?? 0
|
|
465
|
+
}
|
|
466
|
+
const onDragLeave = () => { dragOverState.value = null; dragOverIndex.value = null }
|
|
467
|
+
|
|
468
|
+
// Llamado desde cada tarjeta: calcula si el cursor está en la mitad superior o inferior.
|
|
469
|
+
const onCardDragOver = (e, state, index) => {
|
|
470
|
+
if (!props.reorderEndpoint) return
|
|
471
|
+
e.preventDefault()
|
|
472
|
+
e.stopPropagation()
|
|
473
|
+
dragOverState.value = state
|
|
474
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
475
|
+
const after = (e.clientY - rect.top) > rect.height / 2
|
|
476
|
+
dragOverIndex.value = after ? index + 1 : index
|
|
477
|
+
}
|
|
452
478
|
|
|
453
479
|
const onDrop = async (targetState) => {
|
|
454
|
-
dragOverState.value = null
|
|
455
480
|
const id = draggedId.value
|
|
456
481
|
const fromState = draggedFrom.value
|
|
457
|
-
|
|
482
|
+
const dropIndex = dragOverIndex.value
|
|
483
|
+
dragOverState.value = null
|
|
484
|
+
dragOverIndex.value = null
|
|
485
|
+
draggedId.value = null
|
|
458
486
|
draggedFrom.value = null
|
|
459
|
-
if (!id
|
|
487
|
+
if (!id) return
|
|
488
|
+
|
|
489
|
+
// Modo reorden (prop seteada): calcula vecinos y persiste posición.
|
|
490
|
+
if (props.reorderEndpoint) {
|
|
491
|
+
const colList = (columnRows.value[targetState] ?? []).filter(r => r.id !== id)
|
|
492
|
+
const idx = dropIndex == null ? colList.length : Math.min(dropIndex, colList.length)
|
|
493
|
+
const beforeId = idx > 0 ? colList[idx - 1].id : null // tarjeta arriba
|
|
494
|
+
const afterId = idx < colList.length ? colList[idx].id : null // tarjeta abajo
|
|
495
|
+
|
|
496
|
+
if (fromState === targetState && beforeId == null && afterId == null) return
|
|
497
|
+
|
|
498
|
+
const snapshot = rows.value.map(r => ({ ...r }))
|
|
499
|
+
const ridx = rows.value.findIndex(r => r.id === id)
|
|
500
|
+
if (ridx >= 0) {
|
|
501
|
+
const before = beforeId ? rows.value.find(r => r.id === beforeId) : null
|
|
502
|
+
const after = afterId ? rows.value.find(r => r.id === afterId) : null
|
|
503
|
+
const newPos = before && after ? (before.position + after.position) / 2
|
|
504
|
+
: before ? before.position + 1
|
|
505
|
+
: after ? after.position - 1
|
|
506
|
+
: 1
|
|
507
|
+
rows.value[ridx] = { ...rows.value[ridx], [props.stateKey]: targetState, position: newPos }
|
|
508
|
+
}
|
|
509
|
+
emit('move', { id, from: fromState, to: targetState })
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
await api.post(props.reorderEndpoint, { id, column: targetState, before_id: beforeId, after_id: afterId })
|
|
513
|
+
} catch (e) {
|
|
514
|
+
rows.value = snapshot
|
|
515
|
+
}
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Modo legacy (sin reorderEndpoint): solo cross-column, comportamiento actual.
|
|
520
|
+
if (fromState === targetState) return
|
|
460
521
|
|
|
461
522
|
// Optimistic update local (solo en endpoint mode).
|
|
462
523
|
// En items mode el optimistic lo gestiona el moveMutation externo
|
|
@@ -747,25 +808,34 @@ defineExpose({ reload: fetchAll, rows })
|
|
|
747
808
|
bordered ? 'bg-muted/40' : '',
|
|
748
809
|
]"
|
|
749
810
|
>
|
|
811
|
+
<template v-for="(row, ci) in columnRows[state.key]" :key="row.id">
|
|
812
|
+
<div
|
|
813
|
+
v-if="reorderEndpoint && dragOverState === state.key && dragOverIndex === ci"
|
|
814
|
+
class="rounded-card border-2 border-dashed border-primary/50 bg-primary/5 h-10"
|
|
815
|
+
/>
|
|
816
|
+
<div
|
|
817
|
+
draggable="true"
|
|
818
|
+
@dragstart="onDragStart(row, state.key)"
|
|
819
|
+
@dragover="onCardDragOver($event, state.key, ci)"
|
|
820
|
+
@click="handleCardClick(row)"
|
|
821
|
+
class="bg-card border border-card-line rounded-card p-3 cursor-grab active:cursor-grabbing hover:border-primary/40 hover:shadow-sm transition-all select-none"
|
|
822
|
+
:class="[
|
|
823
|
+
draggedId === row.id ? 'opacity-40' : '',
|
|
824
|
+
previewRow?.id === row.id ? 'ring-2 ring-primary/30' : '',
|
|
825
|
+
]"
|
|
826
|
+
>
|
|
827
|
+
<slot name="card" :row="row" :state="state">
|
|
828
|
+
<div class="space-y-1">
|
|
829
|
+
<p class="text-sm font-medium text-foreground truncate">{{ row.name ?? row.title ?? row.id }}</p>
|
|
830
|
+
<p v-if="row.description" class="text-xs text-muted-foreground line-clamp-2">{{ row.description }}</p>
|
|
831
|
+
</div>
|
|
832
|
+
</slot>
|
|
833
|
+
</div>
|
|
834
|
+
</template>
|
|
750
835
|
<div
|
|
751
|
-
v-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
@dragstart="onDragStart(row, state.key)"
|
|
755
|
-
@click="handleCardClick(row)"
|
|
756
|
-
class="bg-card border border-card-line rounded-card p-3 cursor-grab active:cursor-grabbing hover:border-primary/40 hover:shadow-sm transition-all select-none"
|
|
757
|
-
:class="[
|
|
758
|
-
draggedId === row.id ? 'opacity-40' : '',
|
|
759
|
-
previewRow?.id === row.id ? 'ring-2 ring-primary/30' : '',
|
|
760
|
-
]"
|
|
761
|
-
>
|
|
762
|
-
<slot name="card" :row="row" :state="state">
|
|
763
|
-
<div class="space-y-1">
|
|
764
|
-
<p class="text-sm font-medium text-foreground truncate">{{ row.name ?? row.title ?? row.id }}</p>
|
|
765
|
-
<p v-if="row.description" class="text-xs text-muted-foreground line-clamp-2">{{ row.description }}</p>
|
|
766
|
-
</div>
|
|
767
|
-
</slot>
|
|
768
|
-
</div>
|
|
836
|
+
v-if="reorderEndpoint && dragOverState === state.key && dragOverIndex === (columnRows[state.key]?.length ?? 0) && columnRows[state.key]?.length"
|
|
837
|
+
class="rounded-card border-2 border-dashed border-primary/50 bg-primary/5 h-10"
|
|
838
|
+
/>
|
|
769
839
|
|
|
770
840
|
<div
|
|
771
841
|
v-if="!columnRows[state.key]?.length"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@innertia-solutions/innertia-nuxt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"description": "Innertia Solutions — Nuxt layer unificada: core, auth, multitenancy, theme y app contexts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nuxt",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"vue": ">=3.5.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@floating-ui/dom": "^1.7.6",
|
|
34
35
|
"@nuxtjs/seo": "^3.4.0",
|
|
35
36
|
"@pinia/nuxt": "^0.11.3",
|
|
36
37
|
"@tabler/icons-vue": "^3.44.0",
|
|
@@ -3,6 +3,7 @@ declare global {
|
|
|
3
3
|
HSStaticMethods?: { autoInit?: () => void }
|
|
4
4
|
HSSelect?: new (el: HTMLElement) => { destroy?: () => void }
|
|
5
5
|
HSThemeAppearance?: { init?: () => void }
|
|
6
|
+
FloatingUIDOM?: unknown
|
|
6
7
|
}
|
|
7
8
|
}
|
|
8
9
|
|
|
@@ -10,6 +11,13 @@ export default defineNuxtPlugin(async () => {
|
|
|
10
11
|
if (!process.client) return
|
|
11
12
|
|
|
12
13
|
try {
|
|
14
|
+
// Preline 3 lee `window.FloatingUIDOM` (global, no import) para posicionar
|
|
15
|
+
// dropdowns con `dropdownScope:'window'` (los teletransporta al body y flota
|
|
16
|
+
// con floating-ui, escapando cualquier overflow — ej. selects dentro de modales).
|
|
17
|
+
if (!window.FloatingUIDOM) {
|
|
18
|
+
window.FloatingUIDOM = await import('@floating-ui/dom')
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
await import('preline')
|
|
14
22
|
|
|
15
23
|
const initPreline = () => {
|