- Composant UiDataTable (pagination, slots header/cell/actions/empty) - Composable useDataTableServerState (token anti-race, debounce filtres) - Migration de la page réceptions finies sur le nouveau pattern - pagination_client_items_per_page activé globalement Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import { ref, watch } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
|
|
type FilterValue = string | number | boolean | null
|
|
|
|
export interface UseDataTableServerStateOptions {
|
|
initialPerPage?: number
|
|
debounceMs?: number
|
|
}
|
|
|
|
export function useDataTableServerState<T = Record<string, unknown>>(
|
|
endpoint: string,
|
|
initialFilters: Record<string, FilterValue> = {},
|
|
options: UseDataTableServerStateOptions = {}
|
|
) {
|
|
const api = useApi()
|
|
|
|
const debounceMs = options.debounceMs ?? 300
|
|
const initialPerPage = options.initialPerPage ?? 10
|
|
|
|
const items = ref<T[]>([]) as { value: T[] }
|
|
const totalItems = ref(0)
|
|
const page = ref(1)
|
|
const perPage = ref(initialPerPage)
|
|
const filters = ref<Record<string, FilterValue>>({ ...initialFilters })
|
|
const loading = ref(false)
|
|
|
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
let requestToken = 0
|
|
|
|
const buildQueryParams = (): Record<string, string | number | boolean> => {
|
|
const params: Record<string, string | number | boolean> = {
|
|
page: page.value,
|
|
itemsPerPage: perPage.value
|
|
}
|
|
for (const [key, value] of Object.entries(filters.value)) {
|
|
if (value === '' || value === null) continue
|
|
params[key] = value as string | number | boolean
|
|
}
|
|
return params
|
|
}
|
|
|
|
const fetchItems = async (): Promise<void> => {
|
|
const currentToken = ++requestToken
|
|
loading.value = true
|
|
try {
|
|
const data = await api.get<{ member: T[]; totalItems: number }>(
|
|
endpoint,
|
|
buildQueryParams(),
|
|
{
|
|
toast: false,
|
|
headers: { Accept: 'application/ld+json' }
|
|
}
|
|
)
|
|
if (currentToken !== requestToken) return
|
|
items.value = data?.member ?? []
|
|
totalItems.value = data?.totalItems ?? 0
|
|
} finally {
|
|
if (currentToken === requestToken) {
|
|
loading.value = false
|
|
}
|
|
}
|
|
}
|
|
|
|
const reload = (): void => {
|
|
if (debounceTimer) {
|
|
clearTimeout(debounceTimer)
|
|
debounceTimer = null
|
|
}
|
|
void fetchItems()
|
|
}
|
|
|
|
const scheduleReload = (): void => {
|
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
debounceTimer = setTimeout(() => {
|
|
debounceTimer = null
|
|
void fetchItems()
|
|
}, debounceMs)
|
|
}
|
|
|
|
watch([page, perPage], () => {
|
|
reload()
|
|
})
|
|
|
|
watch(filters, () => {
|
|
if (page.value !== 1) {
|
|
page.value = 1
|
|
return
|
|
}
|
|
scheduleReload()
|
|
}, { deep: true })
|
|
|
|
return {
|
|
items,
|
|
totalItems,
|
|
page,
|
|
perPage,
|
|
filters,
|
|
loading,
|
|
reload
|
|
}
|
|
}
|