Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Replaced legacy components with new `SettingsCard`, `SettingsField`, and `SettingsToggleRow` for a unified design. - Enhanced `AdminListControls` to support compact mode and improved pagination controls. - Updated various sections including `AiAssistantSection`, `AuthenticationSection`, and `DriveMountOAuthSection` to utilize new components, streamlining the settings interface. - Improved accessibility and user experience across admin settings with clearer labels and hints. - Deprecated old components while maintaining backward compatibility for existing admin sections.
180 lines
4.6 KiB
TypeScript
180 lines
4.6 KiB
TypeScript
"use client"
|
||
|
||
import type { ReactNode } from "react"
|
||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Label } from "@/components/ui/label"
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select"
|
||
import { cn } from "@/lib/utils"
|
||
|
||
export type AdminListSortOption = {
|
||
value: string
|
||
label: string
|
||
}
|
||
|
||
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const
|
||
|
||
export function AdminListControls({
|
||
page,
|
||
pageSize,
|
||
total,
|
||
totalPages,
|
||
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
||
sort,
|
||
sortOptions,
|
||
onPageChange,
|
||
onPageSizeChange,
|
||
onSortChange,
|
||
itemLabel,
|
||
compact = false,
|
||
leading,
|
||
}: {
|
||
page: number
|
||
pageSize: number
|
||
total: number
|
||
totalPages: number
|
||
pageSizeOptions?: readonly number[]
|
||
sort: string
|
||
sortOptions: readonly AdminListSortOption[]
|
||
onPageChange: (page: number) => void
|
||
onPageSizeChange: (pageSize: number) => void
|
||
onSortChange: (sort: string) => void
|
||
itemLabel: string
|
||
/** Barre outils dense : labels dans les selects, pagination icônes. */
|
||
compact?: boolean
|
||
/** Champ ou filtre aligné à gauche (ex. recherche). */
|
||
leading?: ReactNode
|
||
}) {
|
||
const rangeStart = total === 0 ? 0 : (page - 1) * pageSize + 1
|
||
const rangeEnd = Math.min(page * pageSize, total)
|
||
const rangeLabel =
|
||
total === 0
|
||
? `0 ${itemLabel}`
|
||
: `${rangeStart.toLocaleString("fr-FR")}–${rangeEnd.toLocaleString("fr-FR")} sur ${total.toLocaleString("fr-FR")} ${itemLabel}`
|
||
|
||
const pageSizeSelect = (
|
||
<Select value={String(pageSize)} onValueChange={(value) => onPageSizeChange(Number(value))}>
|
||
<SelectTrigger
|
||
className={cn("h-9", compact ? "w-18 shrink-0" : "mt-1")}
|
||
aria-label="Éléments par page"
|
||
>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{pageSizeOptions.map((size) => (
|
||
<SelectItem key={size} value={String(size)}>
|
||
{size}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
)
|
||
|
||
const sortSelect = (
|
||
<Select value={sort} onValueChange={onSortChange}>
|
||
<SelectTrigger
|
||
className={cn(
|
||
"h-9",
|
||
compact ? "min-w-36 max-w-48 flex-1 basis-36" : "mt-1",
|
||
)}
|
||
aria-label="Tri"
|
||
>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{sortOptions.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
)
|
||
|
||
const pagination = compact ? (
|
||
<div className="flex shrink-0 gap-1">
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
className="size-8"
|
||
disabled={page <= 1}
|
||
aria-label="Page précédente"
|
||
onClick={() => onPageChange(page - 1)}
|
||
>
|
||
<ChevronLeft className="size-4" />
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
className="size-8"
|
||
disabled={page >= totalPages}
|
||
aria-label="Page suivante"
|
||
onClick={() => onPageChange(page + 1)}
|
||
>
|
||
<ChevronRight className="size-4" />
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
disabled={page <= 1}
|
||
onClick={() => onPageChange(page - 1)}
|
||
>
|
||
Précédent
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
disabled={page >= totalPages}
|
||
onClick={() => onPageChange(page + 1)}
|
||
>
|
||
Suivant
|
||
</Button>
|
||
</div>
|
||
)
|
||
|
||
if (compact) {
|
||
return (
|
||
<div className="mb-3 space-y-2">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
{leading}
|
||
{pageSizeSelect}
|
||
{sortSelect}
|
||
</div>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<p className="min-w-0 truncate text-xs text-muted-foreground">{rangeLabel}</p>
|
||
{pagination}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||
<div className="flex flex-wrap items-end gap-3">
|
||
<div className="w-36">
|
||
<Label className="text-xs">Par page</Label>
|
||
{pageSizeSelect}
|
||
</div>
|
||
<div className="min-w-[200px] flex-1 sm:max-w-xs">
|
||
<Label className="text-xs">Tri</Label>
|
||
{sortSelect}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center justify-between gap-3 sm:justify-end">
|
||
<p className="text-sm text-muted-foreground">{rangeLabel}</p>
|
||
{pagination}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|