fix(slots) : filter slot select options server-side instead of client-side
PieceSelect, ProductSelect and ComposantSelect were loading up to 200 items then filtering client-side by typeId. If the matching items were not in the first 200, the dropdown appeared empty. Now each select component uses API Platform filters (typePiece, typeProduct, typeComposant) to fetch only relevant items server-side, with local state to avoid overwriting the global catalog cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||||
import { useComposants } from '~/composables/useComposants'
|
import { useComposants } from '~/composables/useComposants'
|
||||||
|
|
||||||
@@ -52,43 +52,39 @@ const emit = defineEmits<{
|
|||||||
(e: 'update:modelValue', value: string | null): void
|
(e: 'update:modelValue', value: string | null): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { composants, loading, loadComposants } = useComposants()
|
const { loading: globalLoading, loadComposants } = useComposants()
|
||||||
|
|
||||||
const composantOptions = computed(() => {
|
const localComposants = ref<any[]>([])
|
||||||
const baseOptions = Array.isArray(composants.value) ? composants.value : []
|
const localLoading = ref(false)
|
||||||
if (!props.typeComposantId) {
|
const loading = computed(() => localLoading.value || globalLoading.value)
|
||||||
return baseOptions
|
|
||||||
|
const composantOptions = computed(() => localComposants.value)
|
||||||
|
|
||||||
|
const loadFilteredComposants = async () => {
|
||||||
|
if (!props.typeComposantId) return
|
||||||
|
localLoading.value = true
|
||||||
|
try {
|
||||||
|
const result = await loadComposants({ typeComposantId: props.typeComposantId, itemsPerPage: 500, force: true })
|
||||||
|
if (result.success && result.data?.items) {
|
||||||
|
localComposants.value = result.data.items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (error: unknown) {
|
||||||
const allowedTypeId = String(props.typeComposantId)
|
console.error('Erreur lors du chargement des composants:', error)
|
||||||
return baseOptions.filter((composant: any) => {
|
}
|
||||||
const typeId =
|
finally {
|
||||||
composant?.typeComposantId ||
|
localLoading.value = false
|
||||||
composant?.typeComposant?.id ||
|
}
|
||||||
null
|
}
|
||||||
return typeId ? String(typeId) === allowedTypeId : false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (composantOptions.value.length === 0) {
|
loadFilteredComposants()
|
||||||
loadComposants({ itemsPerPage: 200 }).catch((error: unknown) => {
|
|
||||||
console.error('Erreur lors du chargement des composants:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.typeComposantId,
|
||||||
(value) => {
|
() => {
|
||||||
if (typeof value === 'string' && value) {
|
loadFilteredComposants()
|
||||||
const exists = composantOptions.value.some((c: any) => c.id === value)
|
|
||||||
if (!exists && !loading.value) {
|
|
||||||
loadComposants({ itemsPerPage: 200, force: true }).catch((error: unknown) => {
|
|
||||||
console.error('Erreur lors du chargement des composants:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||||
import { usePieces } from '~/composables/usePieces'
|
import { usePieces } from '~/composables/usePieces'
|
||||||
|
|
||||||
@@ -52,43 +52,39 @@ const emit = defineEmits<{
|
|||||||
(e: 'update:modelValue', value: string | null): void
|
(e: 'update:modelValue', value: string | null): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { pieces, loading, loadPieces } = usePieces()
|
const { loading: globalLoading, loadPieces } = usePieces()
|
||||||
|
|
||||||
const pieceOptions = computed(() => {
|
const localPieces = ref<any[]>([])
|
||||||
const baseOptions = Array.isArray(pieces.value) ? pieces.value : []
|
const localLoading = ref(false)
|
||||||
if (!props.typePieceId) {
|
const loading = computed(() => localLoading.value || globalLoading.value)
|
||||||
return baseOptions
|
|
||||||
|
const pieceOptions = computed(() => localPieces.value)
|
||||||
|
|
||||||
|
const loadFilteredPieces = async () => {
|
||||||
|
if (!props.typePieceId) return
|
||||||
|
localLoading.value = true
|
||||||
|
try {
|
||||||
|
const result = await loadPieces({ typePieceId: props.typePieceId, itemsPerPage: 500, force: true })
|
||||||
|
if (result.success && result.data?.items) {
|
||||||
|
localPieces.value = result.data.items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (error: unknown) {
|
||||||
const allowedTypeId = String(props.typePieceId)
|
console.error('Erreur lors du chargement des pièces:', error)
|
||||||
return baseOptions.filter((piece: any) => {
|
}
|
||||||
const typeId =
|
finally {
|
||||||
piece?.typePieceId ||
|
localLoading.value = false
|
||||||
piece?.typePiece?.id ||
|
}
|
||||||
null
|
}
|
||||||
return typeId ? String(typeId) === allowedTypeId : false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (pieceOptions.value.length === 0) {
|
loadFilteredPieces()
|
||||||
loadPieces({ itemsPerPage: 200 }).catch((error: unknown) => {
|
|
||||||
console.error('Erreur lors du chargement des pièces:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.typePieceId,
|
||||||
(value) => {
|
() => {
|
||||||
if (typeof value === 'string' && value) {
|
loadFilteredPieces()
|
||||||
const exists = pieceOptions.value.some((piece: any) => piece.id === value)
|
|
||||||
if (!exists && !loading.value) {
|
|
||||||
loadPieces({ itemsPerPage: 200, force: true }).catch((error: unknown) => {
|
|
||||||
console.error('Erreur lors du chargement des pièces:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||||
import { useProducts } from '~/composables/useProducts'
|
import { useProducts } from '~/composables/useProducts'
|
||||||
|
|
||||||
@@ -52,43 +52,39 @@ const emit = defineEmits<{
|
|||||||
(e: 'update:modelValue', value: string | null): void
|
(e: 'update:modelValue', value: string | null): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { products, loading, loadProducts } = useProducts()
|
const { loading: globalLoading, loadProducts } = useProducts()
|
||||||
|
|
||||||
const productOptions = computed(() => {
|
const localProducts = ref<any[]>([])
|
||||||
const baseOptions = Array.isArray(products.value) ? products.value : []
|
const localLoading = ref(false)
|
||||||
if (!props.typeProductId) {
|
const loading = computed(() => localLoading.value || globalLoading.value)
|
||||||
return baseOptions
|
|
||||||
|
const productOptions = computed(() => localProducts.value)
|
||||||
|
|
||||||
|
const loadFilteredProducts = async () => {
|
||||||
|
if (!props.typeProductId) return
|
||||||
|
localLoading.value = true
|
||||||
|
try {
|
||||||
|
const result = await loadProducts({ typeProductId: props.typeProductId, itemsPerPage: 500, force: true })
|
||||||
|
if (result.success && result.data?.items) {
|
||||||
|
localProducts.value = result.data.items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (error: unknown) {
|
||||||
const allowedTypeId = String(props.typeProductId)
|
console.error('Erreur lors du chargement des produits:', error)
|
||||||
return baseOptions.filter((product) => {
|
}
|
||||||
const typeId =
|
finally {
|
||||||
product?.typeProductId ||
|
localLoading.value = false
|
||||||
product?.typeProduct?.id ||
|
}
|
||||||
null
|
}
|
||||||
return typeId ? String(typeId) === allowedTypeId : false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (productOptions.value.length === 0) {
|
loadFilteredProducts()
|
||||||
loadProducts().catch((error) => {
|
|
||||||
console.error('Erreur lors du chargement des produits:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.typeProductId,
|
||||||
(value) => {
|
() => {
|
||||||
if (typeof value === 'string' && value) {
|
loadFilteredProducts()
|
||||||
const exists = productOptions.value.some((product) => product.id === value)
|
|
||||||
if (!exists && !loading.value) {
|
|
||||||
loadProducts({ force: true }).catch((error) => {
|
|
||||||
console.error('Erreur lors du chargement des produits:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ interface LoadComposantsOptions {
|
|||||||
orderBy?: string
|
orderBy?: string
|
||||||
orderDir?: 'asc' | 'desc'
|
orderDir?: 'asc' | 'desc'
|
||||||
typeName?: string
|
typeName?: string
|
||||||
|
typeComposantId?: string
|
||||||
force?: boolean
|
force?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,17 +110,18 @@ export function useComposants() {
|
|||||||
orderBy = 'name',
|
orderBy = 'name',
|
||||||
orderDir = 'asc',
|
orderDir = 'asc',
|
||||||
typeName,
|
typeName,
|
||||||
|
typeComposantId,
|
||||||
force = false,
|
force = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
if (!force && loaded.value && !search && !typeName && page === 1) {
|
if (!force && loaded.value && !search && !typeName && !typeComposantId && page === 1) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: composants.value, total: total.value, page, itemsPerPage },
|
data: { items: composants.value, total: total.value, page, itemsPerPage },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading.value) {
|
if (!typeComposantId && loading.value) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: composants.value, total: total.value, page, itemsPerPage },
|
data: { items: composants.value, total: total.value, page, itemsPerPage },
|
||||||
@@ -128,7 +130,6 @@ export function useComposants() {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.set('itemsPerPage', String(itemsPerPage))
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
params.set('page', String(page))
|
params.set('page', String(page))
|
||||||
@@ -141,20 +142,29 @@ export function useComposants() {
|
|||||||
params.set('typeComposant.name', typeName.trim())
|
params.set('typeComposant.name', typeName.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeComposantId) {
|
||||||
|
params.set('typeComposant', typeComposantId)
|
||||||
|
}
|
||||||
|
|
||||||
params.set(`order[${orderBy}]`, orderDir)
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
const result = await get(`/composants?${params.toString()}`)
|
const result = await get(`/composants?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
composants.value = enrichedItems
|
const resultTotal = extractTotal(result.data, items.length)
|
||||||
total.value = extractTotal(result.data, items.length)
|
|
||||||
loaded.value = true
|
if (!typeComposantId) {
|
||||||
|
composants.value = enrichedItems
|
||||||
|
total.value = resultTotal
|
||||||
|
loaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
items: enrichedItems,
|
items: enrichedItems,
|
||||||
total: total.value,
|
total: resultTotal,
|
||||||
page,
|
page,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ interface LoadPiecesOptions {
|
|||||||
orderBy?: string
|
orderBy?: string
|
||||||
orderDir?: 'asc' | 'desc'
|
orderDir?: 'asc' | 'desc'
|
||||||
typeName?: string
|
typeName?: string
|
||||||
|
typePieceId?: string
|
||||||
force?: boolean
|
force?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,17 +120,20 @@ export function usePieces() {
|
|||||||
orderBy = 'name',
|
orderBy = 'name',
|
||||||
orderDir = 'asc',
|
orderDir = 'asc',
|
||||||
typeName,
|
typeName,
|
||||||
|
typePieceId,
|
||||||
force = false,
|
force = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
if (!force && loaded.value && !search && !typeName && page === 1) {
|
// Only use cache for unfiltered full-catalog loads
|
||||||
|
if (!force && loaded.value && !search && !typeName && !typePieceId && page === 1) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: pieces.value, total: total.value, page, itemsPerPage },
|
data: { items: pieces.value, total: total.value, page, itemsPerPage },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading.value) {
|
// For filtered queries, don't block on global loading state
|
||||||
|
if (!typePieceId && loading.value) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: pieces.value, total: total.value, page, itemsPerPage },
|
data: { items: pieces.value, total: total.value, page, itemsPerPage },
|
||||||
@@ -138,7 +142,6 @@ export function usePieces() {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.set('itemsPerPage', String(itemsPerPage))
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
params.set('page', String(page))
|
params.set('page', String(page))
|
||||||
@@ -151,20 +154,30 @@ export function usePieces() {
|
|||||||
params.set('typePiece.name', typeName.trim())
|
params.set('typePiece.name', typeName.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typePieceId) {
|
||||||
|
params.set('typePiece', typePieceId)
|
||||||
|
}
|
||||||
|
|
||||||
params.set(`order[${orderBy}]`, orderDir)
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
const result = await get(`/pieces?${params.toString()}`)
|
const result = await get(`/pieces?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
pieces.value = enrichedItems
|
const resultTotal = extractTotal(result.data, items.length)
|
||||||
total.value = extractTotal(result.data, items.length)
|
|
||||||
loaded.value = true
|
// Only update global cache for unfiltered queries
|
||||||
|
if (!typePieceId) {
|
||||||
|
pieces.value = enrichedItems
|
||||||
|
total.value = resultTotal
|
||||||
|
loaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
items: enrichedItems,
|
items: enrichedItems,
|
||||||
total: total.value,
|
total: resultTotal,
|
||||||
page,
|
page,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface LoadProductsOptions {
|
|||||||
orderBy?: string
|
orderBy?: string
|
||||||
orderDir?: 'asc' | 'desc'
|
orderDir?: 'asc' | 'desc'
|
||||||
typeName?: string
|
typeName?: string
|
||||||
|
typeProductId?: string
|
||||||
force?: boolean
|
force?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,17 +119,18 @@ export function useProducts() {
|
|||||||
orderBy = 'name',
|
orderBy = 'name',
|
||||||
orderDir = 'asc',
|
orderDir = 'asc',
|
||||||
typeName,
|
typeName,
|
||||||
|
typeProductId,
|
||||||
force = false,
|
force = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
if (!force && loaded.value && !search && !typeName && page === 1) {
|
if (!force && loaded.value && !search && !typeName && !typeProductId && page === 1) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: products.value, total: total.value, page, itemsPerPage },
|
data: { items: products.value, total: total.value, page, itemsPerPage },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading.value) {
|
if (!typeProductId && loading.value) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: products.value, total: total.value, page, itemsPerPage },
|
data: { items: products.value, total: total.value, page, itemsPerPage },
|
||||||
@@ -150,20 +152,29 @@ export function useProducts() {
|
|||||||
params.set('typeProduct.name', typeName.trim())
|
params.set('typeProduct.name', typeName.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeProductId) {
|
||||||
|
params.set('typeProduct', typeProductId)
|
||||||
|
}
|
||||||
|
|
||||||
params.set(`order[${orderBy}]`, orderDir)
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
const result = await get(`/products?${params.toString()}`)
|
const result = await get(`/products?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
products.value = enrichedItems
|
const resultTotal = extractTotal(result.data, items.length)
|
||||||
total.value = extractTotal(result.data, items.length)
|
|
||||||
loaded.value = true
|
if (!typeProductId) {
|
||||||
|
products.value = enrichedItems
|
||||||
|
total.value = resultTotal
|
||||||
|
loaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
items: enrichedItems,
|
items: enrichedItems,
|
||||||
total: total.value,
|
total: resultTotal,
|
||||||
page,
|
page,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user