All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 48s
Problema identificado:
- El array de ubicaciones viene como array de strings simples,
no como objetos con {label, value}
- Ejemplo: ["breñales, la union, lempira", "buenos aires, ..."]
Solución implementada:
1. Normalización de datos:
- Crear computed normalizedUbicaciones que transforma el array
- Si el item es string: usa el string como label y value
- Si el item es objeto: usa sus propiedades label y value
- Filtrar null/undefined durante la transformación
2. Agregar prop loading:
- Agregada prop opcional loading?: boolean
- Pasar loading al UInputMenu para mostrar spinner
3. Simplificar lógica de filtrado:
- Usar normalizedUbicaciones como base
- Filtrar por query sin errores de toLowerCase
4. Mejorar selectedUbicacionesObjects:
- Usar normalizedUbicaciones para encontrar seleccionados
- Comparar con value normalizado
Ahora el componente:
- Muestra todas las ubicaciones correctamente
- Funciona con array de strings o array de objetos
- Muestra animación de loading cuando carga datos
- Búsqueda funciona sin errores
139 lines
4.3 KiB
Vue
139 lines
4.3 KiB
Vue
<template>
|
|
<div class="space-y-3">
|
|
<!-- InputMenu for ubicacion search and selection -->
|
|
<UInputMenu
|
|
:model-value="selectedUbicacionesObjects"
|
|
:items="filteredItems"
|
|
:loading="loading"
|
|
multiple
|
|
label-key="label"
|
|
value-key="value"
|
|
icon="i-lucide-map-pin"
|
|
placeholder="Buscar ubicaciones..."
|
|
size="sm"
|
|
ignore-filter
|
|
:ui="{
|
|
root: 'focus-within:ring-1 focus-within:ring-[var(--brand-primary)] transition-shadow',
|
|
base: 'bg-[var(--brand-surface)] text-[var(--brand-text)] border border-[var(--brand-border)] focus:ring-1 focus:ring-[var(--brand-primary)] focus:border-[var(--brand-primary)]',
|
|
placeholder: 'placeholder-[var(--brand-text-muted)]',
|
|
leadingIcon: 'text-[var(--brand-text-muted)]',
|
|
content: 'bg-[var(--brand-surface)] border border-[var(--brand-border)]',
|
|
item: 'text-[var(--brand-text)] data-highlighted:bg-[rgba(224,192,128,0.12)] data-highlighted:text-[var(--brand-text)]',
|
|
itemLeadingIcon: 'text-[var(--brand-text-muted)]',
|
|
tagsItem: 'bg-[rgba(224,192,128,0.14)] border border-[rgba(224,192,128,0.28)] text-[var(--brand-primary)]',
|
|
tagsItemText: 'text-[var(--brand-primary)]',
|
|
tagsItemDelete: 'text-[var(--brand-text-muted)] hover:text-[var(--brand-primary)] hover:bg-[rgba(224,192,128,0.2)]'
|
|
}"
|
|
@update:model-value="onSelectionChange"
|
|
@update:search-term="searchQuery = $event"
|
|
>
|
|
<template #empty>
|
|
<div class="text-center py-2">
|
|
<span class="text-[var(--brand-text-muted)] text-sm">
|
|
No se encontraron ubicaciones
|
|
</span>
|
|
</div>
|
|
</template>
|
|
</UInputMenu>
|
|
|
|
<!-- Selected count and clear all -->
|
|
<div v-if="selectedUbicaciones.length > 0" class="flex items-center justify-between text-sm">
|
|
<span class="text-[var(--brand-text-muted)]">
|
|
{{ selectedUbicaciones.length }} ubicación{{ selectedUbicaciones.length !== 1 ? 'es' : '' }} seleccionada{{ selectedUbicaciones.length !== 1 ? 's' : '' }}
|
|
</span>
|
|
<UButton
|
|
size="xs"
|
|
color="gray"
|
|
variant="link"
|
|
@click="clearAll"
|
|
>
|
|
Limpiar todo
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { InputMenuItem } from '@nuxt/ui'
|
|
|
|
interface UbicacionItem {
|
|
label: string
|
|
value: string
|
|
}
|
|
|
|
const props = defineProps<{
|
|
selectedUbicaciones: string[]
|
|
ubicaciones: UbicacionItem[] | string[]
|
|
loading?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:selectedUbicaciones': [value: string[]]
|
|
}>()
|
|
|
|
// State
|
|
const searchQuery = ref('')
|
|
|
|
// Computed - Normalize ubicaciones to InputMenuItem format
|
|
const normalizedUbicaciones = computed((): InputMenuItem[] => {
|
|
if (!Array.isArray(props.ubicaciones)) return []
|
|
|
|
return props.ubicaciones
|
|
.filter(u => u) // Filter out null/undefined
|
|
.map(u => {
|
|
// If it's already an object with label and value, use it
|
|
if (typeof u === 'object' && u.label && u.value) {
|
|
return { label: u.label, value: u.value }
|
|
}
|
|
// If it's a string, use it as both label and value
|
|
if (typeof u === 'string') {
|
|
return { label: u, value: u }
|
|
}
|
|
return null
|
|
})
|
|
.filter((u): u is InputMenuItem => u !== null)
|
|
})
|
|
|
|
// Computed - Get selected ubicaciones as objects for InputMenu
|
|
const selectedUbicacionesObjects = computed(() => {
|
|
return normalizedUbicaciones.value.filter(u =>
|
|
props.selectedUbicaciones.includes(u.value as string)
|
|
)
|
|
})
|
|
|
|
// Computed - Filter items based on search
|
|
const filteredItems = computed((): InputMenuItem[] => {
|
|
const query = searchQuery.value.trim().toLowerCase()
|
|
|
|
if (!query) {
|
|
return normalizedUbicaciones.value
|
|
}
|
|
|
|
return normalizedUbicaciones.value.filter(u =>
|
|
u.label && typeof u.label === 'string' && u.label.toLowerCase().includes(query)
|
|
)
|
|
})
|
|
|
|
// Methods
|
|
function onSelectionChange(value: any[]) {
|
|
if (!Array.isArray(value)) {
|
|
emit('update:selectedUbicaciones', [])
|
|
return
|
|
}
|
|
|
|
// Extract values from selected items
|
|
const values = value
|
|
.filter(item => item && (item.value || typeof item === 'string'))
|
|
.map(item => {
|
|
if (typeof item === 'string') return item
|
|
return item.value || item
|
|
})
|
|
|
|
emit('update:selectedUbicaciones', values)
|
|
}
|
|
|
|
function clearAll() {
|
|
emit('update:selectedUbicaciones', [])
|
|
}
|
|
</script>
|