@innertia-solutions/innertia-nuxt 0.1.39 → 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 CHANGED
@@ -4,12 +4,13 @@ All notable changes to `@innertia-solutions/innertia-nuxt` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ### Fixed
7
+ ### Added
8
8
 
9
- - `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
+ - `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
10
 
11
11
  ### Fixed
12
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.
13
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.
14
15
 
15
16
  ### BREAKING CHANGES
@@ -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 = (e, state) => { e.preventDefault(); dragOverState.value = state }
451
- const onDragLeave = () => { dragOverState.value = null }
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
- draggedId.value = null
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 || fromState === targetState) return
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-for="row in columnRows[state.key]"
752
- :key="row.id"
753
- draggable="true"
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.39",
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",