@zevra/ui 0.43.1 → 0.44.0
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/DECISIONS.md +32 -0
- package/dist/css-inline.d.ts.map +1 -1
- package/dist/css-inline.js +1 -1
- package/dist/css-inline.js.map +1 -1
- package/dist/data-table.d.ts +29 -3
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +58 -40
- package/dist/data-table.js.map +1 -1
- package/dist/grille.d.ts +91 -8
- package/dist/grille.d.ts.map +1 -1
- package/dist/grille.js +188 -78
- package/dist/grille.js.map +1 -1
- package/package.json +1 -1
- package/react/css-inline.ts +1 -1
- package/react/data-table.tsx +76 -5
- package/react/grille.tsx +454 -164
- package/src/data.css +71 -3
package/react/grille.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useSta
|
|
|
18
18
|
import { createPortal } from 'react-dom'
|
|
19
19
|
import type {
|
|
20
20
|
CSSProperties,
|
|
21
|
+
DragEvent as EvenementGlisser,
|
|
21
22
|
KeyboardEvent as EvenementClavier,
|
|
22
23
|
PointerEvent as EvenementPointeur,
|
|
23
24
|
ReactNode,
|
|
@@ -39,12 +40,12 @@ import {
|
|
|
39
40
|
type FilterFn,
|
|
40
41
|
type OnChangeFn,
|
|
41
42
|
type PaginationState,
|
|
42
|
-
type Row,
|
|
43
43
|
type RowSelectionState,
|
|
44
44
|
type SortingState,
|
|
45
45
|
type Table as TableTanstack,
|
|
46
46
|
type Updater,
|
|
47
47
|
type VisibilityState,
|
|
48
|
+
type Row,
|
|
48
49
|
} from '@tanstack/react-table'
|
|
49
50
|
import {
|
|
50
51
|
DndContext,
|
|
@@ -75,6 +76,7 @@ import { SelectMenu } from './select-menu.js'
|
|
|
75
76
|
import { Menu, MenuItem, Pager, PagerArrow, Popover } from './navigation.js'
|
|
76
77
|
import { Empty } from './feedback.js'
|
|
77
78
|
import { Meta } from './data.js'
|
|
79
|
+
import { Mono } from './typography.js'
|
|
78
80
|
import { cx } from './cx.js'
|
|
79
81
|
|
|
80
82
|
export { usePersistedState }
|
|
@@ -94,6 +96,25 @@ export interface VueGrille {
|
|
|
94
96
|
pageSize: number
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/** La sélection tenue par l'APP : les clés cochées lui appartiennent, elles
|
|
100
|
+
* survivent au changement de filtre, et ce sont elles que ses actions
|
|
101
|
+
* consomment. La case maîtresse porte sur les lignes AFFICHÉES, jamais sur
|
|
102
|
+
* ce qu'un filtre garde hors de l'écran, qui reste coché tel quel. */
|
|
103
|
+
export interface SelectionGrille<T> {
|
|
104
|
+
choisies: ReadonlySet<string>
|
|
105
|
+
onChoisir: (cles: Set<string>) => void
|
|
106
|
+
/** La clé retenue pour une ligne. Par défaut : son identifiant
|
|
107
|
+
* (`getRowId`). */
|
|
108
|
+
cle?: (ligne: T) => string
|
|
109
|
+
/** Ce que le lecteur d'écran annonce pour la case d'une ligne. */
|
|
110
|
+
label?: (ligne: T) => string
|
|
111
|
+
/** Les lignes qui ont une case ; les autres n'en ont pas, et la case
|
|
112
|
+
* maîtresse les ignore. */
|
|
113
|
+
selectionnable?: (ligne: T) => boolean
|
|
114
|
+
/** Cases inertes le temps d'une opération en cours. */
|
|
115
|
+
desactivee?: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
/** Les tailles de page proposées au pied. */
|
|
98
119
|
export const TAILLES_PAGE = [25, 50, 100] as const
|
|
99
120
|
|
|
@@ -114,6 +135,11 @@ export interface TextesGrille {
|
|
|
114
135
|
min: string
|
|
115
136
|
max: string
|
|
116
137
|
effacer: string
|
|
138
|
+
effacerFiltres: string
|
|
139
|
+
toutAfficher: string
|
|
140
|
+
aucun: string
|
|
141
|
+
rechercherValeur: string
|
|
142
|
+
aucuneValeur: string
|
|
117
143
|
/** Raccourcis de période du filtre de dates. */
|
|
118
144
|
periodes: string
|
|
119
145
|
aujourdhui: string
|
|
@@ -169,6 +195,11 @@ export const TEXTES_GRILLE: TextesGrille = {
|
|
|
169
195
|
min: 'Min.',
|
|
170
196
|
max: 'Max.',
|
|
171
197
|
effacer: 'Effacer le filtre',
|
|
198
|
+
effacerFiltres: 'Effacer les filtres',
|
|
199
|
+
toutAfficher: 'Tout afficher',
|
|
200
|
+
aucun: 'Aucun',
|
|
201
|
+
rechercherValeur: 'Rechercher une valeur',
|
|
202
|
+
aucuneValeur: 'Aucune valeur.',
|
|
172
203
|
periodes: 'Périodes',
|
|
173
204
|
aujourdhui: "Aujourd'hui",
|
|
174
205
|
septJours: '7 derniers jours',
|
|
@@ -234,13 +265,65 @@ export interface DataGridProps<T> {
|
|
|
234
265
|
* données se rechargent : par défaut c'est l'INDEX, et une ligne cochée
|
|
235
266
|
* deviendrait une autre ligne après un tri côté serveur. */
|
|
236
267
|
getRowId?: (ligne: T, index: number) => string
|
|
237
|
-
/** Une colonne de cases, épinglée à gauche.
|
|
238
|
-
|
|
268
|
+
/** Une colonne de cases, épinglée à gauche. `true` : la grille tient les
|
|
269
|
+
* cases et remonte les lignes par `onSelectionChange`. Un objet : l'APP
|
|
270
|
+
* tient l'ensemble des clés cochées (elles survivent à un filtre, à une
|
|
271
|
+
* page, à un rechargement), voir `SelectionGrille`. */
|
|
272
|
+
selection?: boolean | SelectionGrille<T>
|
|
239
273
|
onSelectionChange?: (lignes: T[]) => void
|
|
274
|
+
/** Tri délégué au SERVEUR : sur une liste paginée côté serveur, trier dans
|
|
275
|
+
* le navigateur ne trierait que la page affichée. La grille n'ordonne
|
|
276
|
+
* plus rien, elle dit seulement quel tri est demandé. */
|
|
277
|
+
triServeur?: { tri: SortingState; onTri: (tri: SortingState) => void }
|
|
278
|
+
/** Clic droit sur une ligne : l'app ouvre son menu contextuel. */
|
|
279
|
+
onRowContextMenu?: (ligne: T, x: number, y: number) => void
|
|
280
|
+
/** Double clic sur une ligne : « ouvrir pour de bon ». */
|
|
281
|
+
onRowDoubleClick?: (ligne: T) => void
|
|
282
|
+
/** La ligne se glisse (glisser-déposer natif) : l'app remplit le
|
|
283
|
+
* `dataTransfer`. */
|
|
284
|
+
onRowDragStart?: (ligne: T, e: EvenementGlisser<HTMLDivElement>) => void
|
|
285
|
+
/** Une ligne de SAISIE en tête du corps : un contenu par identifiant de
|
|
286
|
+
* colonne. Elle suit l'ordre, la visibilité et l'épinglage des colonnes. */
|
|
287
|
+
ligneSaisie?: Partial<Record<string, ReactNode>>
|
|
288
|
+
/** Des intitulés intercalés quand la clé change d'une ligne à l'autre (une
|
|
289
|
+
* année, un mois). N'a de sens que si le tri suit cette clé. */
|
|
290
|
+
groupe?: { de: (ligne: T) => string; rendu?: (cle: string, lignes: T[]) => ReactNode }
|
|
291
|
+
/** Des lignes qui restent en tête QUEL QUE SOIT le tri : le rang 0 d'abord,
|
|
292
|
+
* puis 1, etc., chacun trié comme le reste (les pièces épinglées). */
|
|
293
|
+
rang?: (ligne: T) => number
|
|
294
|
+
/** Une rangée de totaux sous les lignes, calculée sur les lignes
|
|
295
|
+
* AFFICHÉES (après recherche et filtres) : un contenu par colonne. */
|
|
296
|
+
pied?: (lignes: T[]) => Partial<Record<string, ReactNode>>
|
|
297
|
+
/** Rangées resserrées : un tableau secondaire dans une fiche. */
|
|
298
|
+
dense?: boolean
|
|
299
|
+
/** La grille REMPLIT son parent (colonne flex à hauteur bornée) : seules
|
|
300
|
+
* les lignes défilent, la barre, les intitulés et les filtres restent. */
|
|
301
|
+
remplir?: boolean
|
|
302
|
+
/** Sous les lignes, dans la zone qui défile : le déclencheur d'une liste
|
|
303
|
+
* infinie, qui charge la suite en entrant dans l'écran. */
|
|
304
|
+
apresLignes?: ReactNode
|
|
305
|
+
/** Enveloppe le CADRE du tableau seul (pas la barre) : un voile de
|
|
306
|
+
* rechargement qui laisse la recherche et les filtres utilisables. */
|
|
307
|
+
envelopperTableau?: (tableau: ReactNode) => ReactNode
|
|
308
|
+
/** La barre d'outils (recherche, colonnes, export). Par défaut : oui.
|
|
309
|
+
* Sans elle, les filtres de colonne restent. */
|
|
310
|
+
barre?: boolean
|
|
311
|
+
/** Un champ de recherche fourni par l'app, à la place de celui de la
|
|
312
|
+
* grille : une recherche faite par le serveur, par exemple. */
|
|
313
|
+
champRecherche?: ReactNode
|
|
314
|
+
/** Visibilité des colonnes TENUE PAR L'APP, quand son bouton « Colonnes »
|
|
315
|
+
* vit hors de la grille. Sinon, la vue enregistrée la tient. */
|
|
316
|
+
visibilite?: VisibilityState
|
|
317
|
+
onVisibilite?: (u: Updater<VisibilityState>) => void
|
|
318
|
+
/** Les lignes affichées (après recherche, filtres et tri) et si un filtre
|
|
319
|
+
* est actif : ce qu'un téléchargement de l'app doit reprendre. Appelé
|
|
320
|
+
* quand leur ensemble change, pas à chaque rendu. */
|
|
321
|
+
onLignesAffichees?: (lignes: T[], filtre: boolean) => void
|
|
240
322
|
/** Lignes par page (25, 50 ou 100), ou `false` pour tout afficher. */
|
|
241
323
|
parPage?: (typeof TAILLES_PAGE)[number] | false
|
|
242
|
-
/** Champ de recherche sur toutes les colonnes. Par défaut : oui.
|
|
243
|
-
|
|
324
|
+
/** Champ de recherche sur toutes les colonnes. Par défaut : oui. Une
|
|
325
|
+
* chaîne en est le texte d'invite. */
|
|
326
|
+
recherche?: boolean | string
|
|
244
327
|
/** Rangée de filtres sous les intitulés. Par défaut : oui. */
|
|
245
328
|
filtres?: boolean
|
|
246
329
|
/** Bouton d'export CSV des lignes filtrées et triées, colonnes visibles.
|
|
@@ -347,7 +430,7 @@ export type TypeFiltre = 'texte' | 'nombre' | 'date' | 'choix'
|
|
|
347
430
|
/** La valeur d'un filtre de colonne, telle que TanStack la tient. */
|
|
348
431
|
export type ValeurFiltre =
|
|
349
432
|
| { type: 'texte'; valeur: string }
|
|
350
|
-
| { type: 'choix';
|
|
433
|
+
| { type: 'choix'; valeurs: string[] }
|
|
351
434
|
| { type: 'nombre'; min: string; max: string }
|
|
352
435
|
| { type: 'date'; du: string; au: string }
|
|
353
436
|
|
|
@@ -398,15 +481,23 @@ const nombre = (s: string): number | null => {
|
|
|
398
481
|
return Number.isFinite(n) ? n : null
|
|
399
482
|
}
|
|
400
483
|
|
|
484
|
+
/** Sans casse ni accents : « decision » trouve « Décision ». */
|
|
485
|
+
export function normaliser(v: unknown): string {
|
|
486
|
+
return String(v ?? '')
|
|
487
|
+
.toLowerCase()
|
|
488
|
+
.normalize('NFD')
|
|
489
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
490
|
+
}
|
|
491
|
+
|
|
401
492
|
/** Le filtre de toutes les colonnes de la grille : il lit la FORME de la
|
|
402
493
|
* valeur, pas la colonne. Bornes comprises. */
|
|
403
494
|
export const filtreGrille: FilterFn<any> = (ligne, colonne, filtre: ValeurFiltre) => {
|
|
404
495
|
const brut = ligne.getValue(colonne)
|
|
405
496
|
switch (filtre?.type) {
|
|
406
497
|
case 'texte':
|
|
407
|
-
return
|
|
498
|
+
return normaliser(brut).includes(normaliser(filtre.valeur.trim()))
|
|
408
499
|
case 'choix':
|
|
409
|
-
return String(brut ?? '')
|
|
500
|
+
return filtre.valeurs.includes(String(brut ?? ''))
|
|
410
501
|
case 'nombre': {
|
|
411
502
|
const v = typeof brut === 'number' ? brut : nombre(String(brut ?? ''))
|
|
412
503
|
const min = nombre(filtre.min)
|
|
@@ -426,7 +517,7 @@ export const filtreGrille: FilterFn<any> = (ligne, colonne, filtre: ValeurFiltre
|
|
|
426
517
|
filtreGrille.autoRemove = (f: ValeurFiltre | undefined) =>
|
|
427
518
|
!f ||
|
|
428
519
|
(f.type === 'texte' && f.valeur.trim() === '') ||
|
|
429
|
-
(f.type === 'choix' && f.
|
|
520
|
+
(f.type === 'choix' && f.valeurs.length === 0) ||
|
|
430
521
|
(f.type === 'nombre' && f.min === '' && f.max === '') ||
|
|
431
522
|
(f.type === 'date' && f.du === '' && f.au === '')
|
|
432
523
|
|
|
@@ -685,28 +776,34 @@ function PoigneeLargeur<T>({
|
|
|
685
776
|
|
|
686
777
|
/* ─── Cases de sélection ─────────────────────────────────────────────── */
|
|
687
778
|
|
|
688
|
-
|
|
779
|
+
type OptionsCases<T> = { current: SelectionGrille<T> | null }
|
|
780
|
+
|
|
781
|
+
function CaseTout<T>({ table, t, options }: { table: TableTanstack<T>; t: TextesGrille; options: OptionsCases<T> }) {
|
|
689
782
|
// Une enveloppe plutôt qu'une ref sur la case : `Checkbox` ne transmet
|
|
690
783
|
// pas de ref sous React 18, que le paquet accepte encore.
|
|
691
784
|
const ref = useRef<HTMLSpanElement>(null)
|
|
692
785
|
const partiel = table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()
|
|
693
786
|
// `indeterminate` n'a pas d'attribut HTML : seule la propriété DOM existe.
|
|
787
|
+
// Sans elle, « 3 sur 40 cochées » ressemble à « aucune ».
|
|
694
788
|
useEffect(() => {
|
|
695
789
|
const caseTout = ref.current?.querySelector('input')
|
|
696
790
|
if (caseTout) caseTout.indeterminate = partiel
|
|
697
791
|
}, [partiel])
|
|
792
|
+
const aucune = !table.getPrePaginationRowModel().rows.some((r) => r.getCanSelect())
|
|
698
793
|
return (
|
|
699
794
|
<span ref={ref} className="zv-cell--case">
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
795
|
+
<Checkbox
|
|
796
|
+
checked={table.getIsAllRowsSelected()}
|
|
797
|
+
disabled={aucune || options.current?.desactivee}
|
|
798
|
+
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
799
|
+
aria-label={t.toutSelectionner}
|
|
800
|
+
title={t.toutSelectionner}
|
|
801
|
+
/>
|
|
705
802
|
</span>
|
|
706
803
|
)
|
|
707
804
|
}
|
|
708
805
|
|
|
709
|
-
function colonneSelection<T>(t: TextesGrille): ColumnDef<T, any> {
|
|
806
|
+
function colonneSelection<T>(t: TextesGrille, options: OptionsCases<T>): ColumnDef<T, any> {
|
|
710
807
|
return {
|
|
711
808
|
id: COLONNE_SELECTION,
|
|
712
809
|
size: 40,
|
|
@@ -717,19 +814,22 @@ function colonneSelection<T>(t: TextesGrille): ColumnDef<T, any> {
|
|
|
717
814
|
enableColumnFilter: false,
|
|
718
815
|
enableGlobalFilter: false,
|
|
719
816
|
enablePinning: false,
|
|
720
|
-
header: ({ table }) => <CaseTout table={table} t={t} />,
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
817
|
+
header: ({ table }) => <CaseTout table={table} t={t} options={options} />,
|
|
818
|
+
// Une ligne qui ne se sélectionne pas n'a PAS de case : une case grisée
|
|
819
|
+
// promettrait une action qui n'existe pas pour elle.
|
|
820
|
+
cell: ({ row }: { row: Row<T> }) =>
|
|
821
|
+
row.getCanSelect() ? (
|
|
822
|
+
<Checkbox
|
|
823
|
+
checked={row.getIsSelected()}
|
|
824
|
+
disabled={options.current?.desactivee}
|
|
825
|
+
onChange={row.getToggleSelectedHandler()}
|
|
826
|
+
// La case vit dans une ligne cliquable : sans ces deux arrêts,
|
|
827
|
+
// cocher ouvrirait aussi la ligne, et Espace l'ouvrirait au clavier.
|
|
828
|
+
onClick={(e) => e.stopPropagation()}
|
|
829
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
830
|
+
aria-label={options.current?.label?.(row.original) ?? t.selectionnerLigne}
|
|
831
|
+
/>
|
|
832
|
+
) : null,
|
|
733
833
|
meta: { classe: 'zv-cell--case' },
|
|
734
834
|
}
|
|
735
835
|
}
|
|
@@ -778,6 +878,15 @@ function MenuColonnes<T>({ table, t }: { table: TableTanstack<T>; t: TextesGrill
|
|
|
778
878
|
</Button>
|
|
779
879
|
{ouvert && (
|
|
780
880
|
<Menu role="group" aria-label={t.colonnesAffichees} className="zv-grille__menu">
|
|
881
|
+
{colonnes.some((c) => !c.getIsVisible()) && (
|
|
882
|
+
<Button
|
|
883
|
+
variante="fantome"
|
|
884
|
+
compact
|
|
885
|
+
onClick={() => table.setColumnVisibility((v) => Object.fromEntries(Object.keys(v).map((k) => [k, true])))}
|
|
886
|
+
>
|
|
887
|
+
{t.toutAfficher}
|
|
888
|
+
</Button>
|
|
889
|
+
)}
|
|
781
890
|
{colonnes.map((c) => {
|
|
782
891
|
const nom = libelleColonne(c)
|
|
783
892
|
const cote = c.getIsPinned()
|
|
@@ -978,59 +1087,94 @@ function FiltreFlottant({
|
|
|
978
1087
|
)
|
|
979
1088
|
}
|
|
980
1089
|
|
|
981
|
-
/** Les flèches promènent le focus d'une option à l'autre, Origine et Fin
|
|
982
|
-
* vont aux extrémités : le parcours clavier d'une liste, que le <select>
|
|
983
|
-
* natif donnait et qu'une liste dessinée doit rendre. */
|
|
984
|
-
function parcourirListe(e: EvenementClavier<HTMLElement>) {
|
|
985
|
-
const options = [...e.currentTarget.querySelectorAll<HTMLElement>('[role="option"]')]
|
|
986
|
-
const i = options.indexOf(e.target as HTMLElement)
|
|
987
|
-
const vers =
|
|
988
|
-
e.key === 'ArrowDown' ? i + 1 : e.key === 'ArrowUp' ? i - 1 : e.key === 'Home' ? 0 : e.key === 'End' ? options.length - 1 : null
|
|
989
|
-
if (vers === null) return
|
|
990
|
-
e.preventDefault()
|
|
991
|
-
options[Math.max(0, Math.min(options.length - 1, vers))]?.focus()
|
|
992
|
-
}
|
|
993
|
-
|
|
994
1090
|
function FiltreChoix<T>({ colonne, nom, t }: { colonne: Column<T, unknown>; nom: string; t: TextesGrille }) {
|
|
995
1091
|
const filtre = colonne.getFilterValue() as ValeurFiltre | undefined
|
|
996
|
-
|
|
997
|
-
//
|
|
998
|
-
|
|
1092
|
+
const choisies = filtre?.type === 'choix' ? filtre.valeurs : []
|
|
1093
|
+
// Les valeurs du modèle FACETTÉ, avec leur nombre de lignes : celles que
|
|
1094
|
+
// les autres filtres laissent encore passer. Une option qui ne rendrait
|
|
1095
|
+
// aucune ligne n'est pas offerte, sauf si elle est déjà cochée : sinon
|
|
1096
|
+
// la liste mentirait sur ce qui filtre.
|
|
1097
|
+
const comptes = colonne.getFacetedUniqueValues()
|
|
1098
|
+
const options = [...comptes.keys()]
|
|
999
1099
|
.filter((v) => v != null && v !== '')
|
|
1000
1100
|
.map(String)
|
|
1001
|
-
.sort((a, b) => a.localeCompare(b, 'fr'))
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (valeur && !options.includes(valeur)) options.unshift(valeur)
|
|
1101
|
+
.sort((a, b) => a.localeCompare(b, 'fr', { numeric: true }))
|
|
1102
|
+
for (const c of choisies) if (!options.includes(c)) options.unshift(c)
|
|
1103
|
+
const ecrire = (valeurs: string[]) => colonne.setFilterValue({ type: 'choix', valeurs })
|
|
1104
|
+
const resume = choisies.length === 0 ? t.tous : choisies.length === 1 ? (choisies[0] ?? t.tous) : `${choisies.length} / ${options.length}`
|
|
1006
1105
|
|
|
1007
1106
|
return (
|
|
1008
1107
|
<FiltreFlottant
|
|
1009
1108
|
libelle={t.filtrerColonne(nom)}
|
|
1010
|
-
resume={
|
|
1011
|
-
vide={
|
|
1109
|
+
resume={resume}
|
|
1110
|
+
vide={choisies.length === 0}
|
|
1012
1111
|
icone="chevron-bas"
|
|
1013
|
-
role="
|
|
1014
|
-
panneau={(
|
|
1015
|
-
<Menu role="listbox" aria-label={t.filtrerColonne(nom)} className="zv-grille__liste" onKeyDown={parcourirListe}>
|
|
1016
|
-
{['', ...options].map((o) => (
|
|
1017
|
-
<MenuItem
|
|
1018
|
-
key={o || '\u0000'}
|
|
1019
|
-
actif={o === valeur}
|
|
1020
|
-
onClick={() => {
|
|
1021
|
-
colonne.setFilterValue({ type: 'choix', valeur: o })
|
|
1022
|
-
fermer()
|
|
1023
|
-
}}
|
|
1024
|
-
>
|
|
1025
|
-
{o || t.tous}
|
|
1026
|
-
</MenuItem>
|
|
1027
|
-
))}
|
|
1028
|
-
</Menu>
|
|
1029
|
-
)}
|
|
1112
|
+
role="dialog"
|
|
1113
|
+
panneau={() => <ListeChoix options={options} comptes={comptes} choisies={choisies} ecrire={ecrire} nom={nom} t={t} />}
|
|
1030
1114
|
/>
|
|
1031
1115
|
)
|
|
1032
1116
|
}
|
|
1033
1117
|
|
|
1118
|
+
/** Plusieurs valeurs à la fois (« Acte » OU « Contrat ») : des cases, avec
|
|
1119
|
+
* le nombre de lignes de chacune, une recherche au-delà de huit options,
|
|
1120
|
+
* et « Tout » / « Aucun » sur ce que la recherche montre. */
|
|
1121
|
+
function ListeChoix({
|
|
1122
|
+
options,
|
|
1123
|
+
comptes,
|
|
1124
|
+
choisies,
|
|
1125
|
+
ecrire,
|
|
1126
|
+
nom,
|
|
1127
|
+
t,
|
|
1128
|
+
}: {
|
|
1129
|
+
options: string[]
|
|
1130
|
+
comptes: Map<unknown, number>
|
|
1131
|
+
choisies: string[]
|
|
1132
|
+
ecrire: (valeurs: string[]) => void
|
|
1133
|
+
nom: string
|
|
1134
|
+
t: TextesGrille
|
|
1135
|
+
}) {
|
|
1136
|
+
const [q, setQ] = useState('')
|
|
1137
|
+
const vues = q.trim() ? options.filter((o) => normaliser(o).includes(normaliser(q.trim()))) : options
|
|
1138
|
+
return (
|
|
1139
|
+
<Popover className="zv-grille__plage" role="dialog" aria-label={t.filtrerColonne(nom)}>
|
|
1140
|
+
{options.length > 8 && (
|
|
1141
|
+
<Search value={q} onChange={(e) => setQ(e.target.value)} placeholder={t.rechercherValeur} aria-label={t.rechercherValeur} />
|
|
1142
|
+
)}
|
|
1143
|
+
<div className="zv-grille__menu-ligne">
|
|
1144
|
+
<Button variante="fantome" compact onClick={() => ecrire([...new Set([...choisies, ...vues])])}>
|
|
1145
|
+
{t.tous}
|
|
1146
|
+
</Button>
|
|
1147
|
+
<Button variante="fantome" compact onClick={() => ecrire(choisies.filter((c) => !vues.includes(c)))}>
|
|
1148
|
+
{t.aucun}
|
|
1149
|
+
</Button>
|
|
1150
|
+
</div>
|
|
1151
|
+
<div className="zv-grille__liste" role="group" aria-label={t.filtrerColonne(nom)}>
|
|
1152
|
+
{vues.map((o) => {
|
|
1153
|
+
const pris = choisies.includes(o)
|
|
1154
|
+
return (
|
|
1155
|
+
<label key={o} className="zv-menu__item zv-grille__menu-case">
|
|
1156
|
+
<Checkbox
|
|
1157
|
+
checked={pris}
|
|
1158
|
+
onChange={() => ecrire(pris ? choisies.filter((c) => c !== o) : [...choisies, o])}
|
|
1159
|
+
/>
|
|
1160
|
+
<span className="zv-colhead__label">{o}</span>
|
|
1161
|
+
<Meta taille="sm" discret className="zv-grille__compte">
|
|
1162
|
+
{comptes.get(o) ?? 0}
|
|
1163
|
+
</Meta>
|
|
1164
|
+
</label>
|
|
1165
|
+
)
|
|
1166
|
+
})}
|
|
1167
|
+
{vues.length === 0 && <Meta taille="sm">{t.aucuneValeur}</Meta>}
|
|
1168
|
+
</div>
|
|
1169
|
+
{choisies.length > 0 && (
|
|
1170
|
+
<Button variante="fantome" compact icone="fermer" onClick={() => ecrire([])}>
|
|
1171
|
+
{t.effacer}
|
|
1172
|
+
</Button>
|
|
1173
|
+
)}
|
|
1174
|
+
</Popover>
|
|
1175
|
+
)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1034
1178
|
function FiltrePlage<T>({
|
|
1035
1179
|
colonne,
|
|
1036
1180
|
type,
|
|
@@ -1155,6 +1299,23 @@ export function DataGrid<T>({
|
|
|
1155
1299
|
getRowId,
|
|
1156
1300
|
selection = false,
|
|
1157
1301
|
onSelectionChange,
|
|
1302
|
+
triServeur,
|
|
1303
|
+
onRowContextMenu,
|
|
1304
|
+
onRowDoubleClick,
|
|
1305
|
+
onRowDragStart,
|
|
1306
|
+
ligneSaisie,
|
|
1307
|
+
groupe,
|
|
1308
|
+
rang,
|
|
1309
|
+
pied,
|
|
1310
|
+
dense,
|
|
1311
|
+
remplir,
|
|
1312
|
+
apresLignes,
|
|
1313
|
+
envelopperTableau,
|
|
1314
|
+
barre = true,
|
|
1315
|
+
champRecherche,
|
|
1316
|
+
visibilite,
|
|
1317
|
+
onVisibilite,
|
|
1318
|
+
onLignesAffichees,
|
|
1158
1319
|
parPage = 25,
|
|
1159
1320
|
recherche = true,
|
|
1160
1321
|
filtres = true,
|
|
@@ -1203,7 +1364,7 @@ export function DataGrid<T>({
|
|
|
1203
1364
|
|
|
1204
1365
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
1205
1366
|
const [globalFilter, setGlobalFilter] = useState('')
|
|
1206
|
-
const [
|
|
1367
|
+
const [selectionInterne, setSelectionInterne] = useState<RowSelectionState>({})
|
|
1207
1368
|
const [pageIndex, setPageIndex] = useState(0)
|
|
1208
1369
|
|
|
1209
1370
|
const champ =
|
|
@@ -1211,10 +1372,44 @@ export function DataGrid<T>({
|
|
|
1211
1372
|
(u) =>
|
|
1212
1373
|
setVue((v) => ({ ...v, [k]: appliquer(u, v[k]) }))
|
|
1213
1374
|
|
|
1375
|
+
/* Sélection. Tenue par la grille (`true`) ou par l'app (un objet) : dans le
|
|
1376
|
+
* second cas, l'état de TanStack se DÉDUIT des clés de l'app à chaque rendu,
|
|
1377
|
+
* et chaque changement lui est rendu en clés. */
|
|
1378
|
+
const avecCases = selection !== false
|
|
1379
|
+
const externe = typeof selection === 'object' ? selection : null
|
|
1380
|
+
const idLigne = (l: T, i: number) => getRowId?.(l, i) ?? String(i)
|
|
1381
|
+
const cleDe = (l: T, i: number) => externe?.cle?.(l) ?? idLigne(l, i)
|
|
1382
|
+
const rowSelection: RowSelectionState = externe
|
|
1383
|
+
? Object.fromEntries(
|
|
1384
|
+
// Une ligne sans case ne se peint pas cochée, même si sa clé est
|
|
1385
|
+
// dans l'ensemble de l'app : on la verrait choisie sans pouvoir
|
|
1386
|
+
// la décocher.
|
|
1387
|
+
data.flatMap((l, i) =>
|
|
1388
|
+
externe.choisies.has(cleDe(l, i)) && externe.selectionnable?.(l) !== false
|
|
1389
|
+
? [[idLigne(l, i), true]]
|
|
1390
|
+
: [],
|
|
1391
|
+
),
|
|
1392
|
+
)
|
|
1393
|
+
: selectionInterne
|
|
1394
|
+
const surSelectionLignes: OnChangeFn<RowSelectionState> = (u) => {
|
|
1395
|
+
if (!externe) return setSelectionInterne(u)
|
|
1396
|
+
const n = appliquer(u, rowSelection)
|
|
1397
|
+
const suite = new Set(externe.choisies)
|
|
1398
|
+
// Seules les clés des lignes PRÉSENTES changent : une ligne cochée puis
|
|
1399
|
+
// filtrée hors de l'écran, ou sur une autre page, reste cochée.
|
|
1400
|
+
data.forEach((l, i) => {
|
|
1401
|
+
if (externe.selectionnable?.(l) === false) return
|
|
1402
|
+
const cle = cleDe(l, i)
|
|
1403
|
+
if (n[idLigne(l, i)]) suite.add(cle)
|
|
1404
|
+
else suite.delete(cle)
|
|
1405
|
+
})
|
|
1406
|
+
externe.onChoisir(suite)
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1214
1409
|
// La colonne de cases est épinglée à gauche, toujours, et n'entre pas dans
|
|
1215
1410
|
// la vue enregistrée : elle dépend de la prop, pas de l'utilisateur.
|
|
1216
1411
|
const pinning: ColumnPinningState = {
|
|
1217
|
-
left:
|
|
1412
|
+
left: avecCases ? [COLONNE_SELECTION, ...(vue.columnPinning.left ?? [])] : vue.columnPinning.left,
|
|
1218
1413
|
right: vue.columnPinning.right,
|
|
1219
1414
|
}
|
|
1220
1415
|
const pagination: PaginationState = {
|
|
@@ -1222,11 +1417,17 @@ export function DataGrid<T>({
|
|
|
1222
1417
|
pageSize: parPage === false ? Math.max(data.length, 1) : vue.pageSize,
|
|
1223
1418
|
}
|
|
1224
1419
|
|
|
1420
|
+
// L'option de sélection est lue AU RENDU par les cases : les colonnes n'ont
|
|
1421
|
+
// pas à se recalculer à chaque case cochée.
|
|
1422
|
+
const optionsCases = useRef(externe)
|
|
1423
|
+
optionsCases.current = externe
|
|
1225
1424
|
const colonnes = useMemo<ColumnDef<T, any>[]>(() => {
|
|
1226
1425
|
const avecFiltre = columns.map((c) => (c.filterFn ? c : { ...c, filterFn: filtreGrille }))
|
|
1227
|
-
return
|
|
1426
|
+
return avecCases ? [colonneSelection<T>(t, optionsCases), ...avecFiltre] : avecFiltre
|
|
1228
1427
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1229
|
-
}, [columns,
|
|
1428
|
+
}, [columns, avecCases, t.toutSelectionner, t.selectionnerLigne])
|
|
1429
|
+
|
|
1430
|
+
const visibiliteCourante = visibilite ?? vue.columnVisibility
|
|
1230
1431
|
|
|
1231
1432
|
const table = useReactTable<T>({
|
|
1232
1433
|
data,
|
|
@@ -1234,10 +1435,10 @@ export function DataGrid<T>({
|
|
|
1234
1435
|
getRowId,
|
|
1235
1436
|
state: {
|
|
1236
1437
|
columnOrder: vue.columnOrder,
|
|
1237
|
-
columnVisibility:
|
|
1438
|
+
columnVisibility: visibiliteCourante,
|
|
1238
1439
|
columnSizing: vue.columnSizing,
|
|
1239
1440
|
columnPinning: pinning,
|
|
1240
|
-
sorting: vue.sorting,
|
|
1441
|
+
sorting: triServeur ? triServeur.tri : vue.sorting,
|
|
1241
1442
|
columnFilters,
|
|
1242
1443
|
globalFilter,
|
|
1243
1444
|
rowSelection,
|
|
@@ -1245,13 +1446,18 @@ export function DataGrid<T>({
|
|
|
1245
1446
|
},
|
|
1246
1447
|
defaultColumn: { size: 150, minSize: 60 },
|
|
1247
1448
|
onColumnOrderChange: champ('columnOrder'),
|
|
1248
|
-
onColumnVisibilityChange:
|
|
1449
|
+
onColumnVisibilityChange: onVisibilite
|
|
1450
|
+
? (u) => onVisibilite((v) => appliquer(u, v))
|
|
1451
|
+
: champ('columnVisibility'),
|
|
1249
1452
|
onColumnSizingChange: champ('columnSizing'),
|
|
1250
|
-
onSortingChange:
|
|
1453
|
+
onSortingChange: triServeur
|
|
1454
|
+
? (u) => triServeur.onTri(appliquer(u, triServeur.tri))
|
|
1455
|
+
: champ('sorting'),
|
|
1456
|
+
manualSorting: Boolean(triServeur),
|
|
1251
1457
|
onColumnPinningChange: (u) =>
|
|
1252
1458
|
setVue((v) => {
|
|
1253
1459
|
const actuel = {
|
|
1254
|
-
left:
|
|
1460
|
+
left: avecCases ? [COLONNE_SELECTION, ...(v.columnPinning.left ?? [])] : v.columnPinning.left,
|
|
1255
1461
|
right: v.columnPinning.right,
|
|
1256
1462
|
}
|
|
1257
1463
|
const n = appliquer(u, actuel)
|
|
@@ -1265,7 +1471,7 @@ export function DataGrid<T>({
|
|
|
1265
1471
|
}),
|
|
1266
1472
|
onColumnFiltersChange: setColumnFilters,
|
|
1267
1473
|
onGlobalFilterChange: setGlobalFilter,
|
|
1268
|
-
onRowSelectionChange:
|
|
1474
|
+
onRowSelectionChange: surSelectionLignes,
|
|
1269
1475
|
onPaginationChange: (u) => {
|
|
1270
1476
|
const n = appliquer(u, pagination)
|
|
1271
1477
|
setPageIndex(n.pageIndex)
|
|
@@ -1273,14 +1479,17 @@ export function DataGrid<T>({
|
|
|
1273
1479
|
setVue((v) => ({ ...v, pageSize: n.pageSize }))
|
|
1274
1480
|
}
|
|
1275
1481
|
},
|
|
1276
|
-
enableRowSelection:
|
|
1482
|
+
enableRowSelection: externe?.selectionnable
|
|
1483
|
+
? (r: Row<T>) => externe.selectionnable!(r.original)
|
|
1484
|
+
: avecCases,
|
|
1277
1485
|
enableMultiSort: true,
|
|
1278
1486
|
enableColumnResizing: true,
|
|
1279
1487
|
columnResizeMode: 'onChange',
|
|
1280
1488
|
enableColumnPinning: true,
|
|
1281
|
-
|
|
1489
|
+
// Sans casse ni accents, comme les filtres de colonne.
|
|
1490
|
+
globalFilterFn: (ligne, colonne, q) => normaliser(ligne.getValue(colonne)).includes(normaliser(String(q).trim())),
|
|
1282
1491
|
getCoreRowModel: getCoreRowModel(),
|
|
1283
|
-
getSortedRowModel: getSortedRowModel(),
|
|
1492
|
+
getSortedRowModel: triServeur ? undefined : getSortedRowModel(),
|
|
1284
1493
|
getFilteredRowModel: getFilteredRowModel(),
|
|
1285
1494
|
getPaginationRowModel: getPaginationRowModel(),
|
|
1286
1495
|
getFacetedRowModel: getFacetedRowModel(),
|
|
@@ -1292,9 +1501,10 @@ export function DataGrid<T>({
|
|
|
1292
1501
|
const surSelection = useRef(onSelectionChange)
|
|
1293
1502
|
surSelection.current = onSelectionChange
|
|
1294
1503
|
useEffect(() => {
|
|
1504
|
+
if (externe) return
|
|
1295
1505
|
surSelection.current?.(table.getSelectedRowModel().rows.map((r) => r.original))
|
|
1296
1506
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1297
|
-
}, [
|
|
1507
|
+
}, [selectionInterne])
|
|
1298
1508
|
|
|
1299
1509
|
// Une page au-delà de la dernière (filtre resserré, données rechargées)
|
|
1300
1510
|
// afficherait la ligne d'absence alors que des lignes existent.
|
|
@@ -1303,18 +1513,22 @@ export function DataGrid<T>({
|
|
|
1303
1513
|
if (pageIndex > 0 && pageIndex >= nbPages) setPageIndex(Math.max(0, nbPages - 1))
|
|
1304
1514
|
}, [pageIndex, nbPages])
|
|
1305
1515
|
|
|
1306
|
-
const
|
|
1307
|
-
reinitialiserVue()
|
|
1516
|
+
const effacerFiltres = () => {
|
|
1308
1517
|
setColumnFilters([])
|
|
1309
1518
|
setGlobalFilter('')
|
|
1519
|
+
}
|
|
1520
|
+
const reinitialiser = () => {
|
|
1521
|
+
reinitialiserVue()
|
|
1522
|
+
if (onVisibilite) onVisibilite(() => defaut.columnVisibility)
|
|
1523
|
+
effacerFiltres()
|
|
1310
1524
|
setPageIndex(0)
|
|
1311
1525
|
}
|
|
1312
1526
|
|
|
1313
1527
|
/* Glisser-déposer des en-têtes. */
|
|
1314
1528
|
const idDnd = useId()
|
|
1315
1529
|
const capteurs = useSensors(
|
|
1316
|
-
// 4 px avant de saisir : sans ce seuil, un simple clic sur
|
|
1317
|
-
// démarre un déplacement.
|
|
1530
|
+
// 4 px avant de saisir : sans ce seuil, un simple clic sur l'en-tête
|
|
1531
|
+
// démarre un déplacement au lieu de trier.
|
|
1318
1532
|
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
1319
1533
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
1320
1534
|
)
|
|
@@ -1357,94 +1571,170 @@ export function DataGrid<T>({
|
|
|
1357
1571
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1358
1572
|
}, [lignesBase, colonnes])
|
|
1359
1573
|
const filtrables = table.getVisibleLeafColumns().some((c) => c.getCanFilter() && types[c.id])
|
|
1574
|
+
|
|
1575
|
+
/** Une rangée qui suit les colonnes affichées (ordre, épinglage) : les
|
|
1576
|
+
* filtres, la ligne de saisie, le pied. */
|
|
1577
|
+
const rangee = (classe: string, contenu: (c: Column<T, unknown>) => ReactNode) => (
|
|
1578
|
+
<div className={classe}>
|
|
1579
|
+
{colonnesAffichees(table).map((c) => {
|
|
1580
|
+
const pin = epinglage(c, table)
|
|
1581
|
+
const registre = c.columnDef.meta?.registre
|
|
1582
|
+
return (
|
|
1583
|
+
<div
|
|
1584
|
+
key={c.id}
|
|
1585
|
+
className={cx('zv-cell', registre === 'num' && 'zv-cell--num', registre === 'nom' && 'zv-cell--name', pin.classes)}
|
|
1586
|
+
style={pin.style}
|
|
1587
|
+
>
|
|
1588
|
+
{contenu(c)}
|
|
1589
|
+
</div>
|
|
1590
|
+
)
|
|
1591
|
+
})}
|
|
1592
|
+
</div>
|
|
1593
|
+
)
|
|
1360
1594
|
const rangeeFiltres =
|
|
1361
|
-
filtres && filtrables
|
|
1362
|
-
|
|
1363
|
-
{colonnesAffichees(table).map((c) => {
|
|
1364
|
-
const pin = epinglage(c, table)
|
|
1595
|
+
filtres && filtrables
|
|
1596
|
+
? rangee('zv-table__head zv-table__head--filtres', (c) => {
|
|
1365
1597
|
const type = c.getCanFilter() ? types[c.id] : false
|
|
1366
|
-
return
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1598
|
+
return type ? <FiltreColonne colonne={c} type={type} t={t} /> : null
|
|
1599
|
+
})
|
|
1600
|
+
: null
|
|
1601
|
+
|
|
1602
|
+
/* Lignes : le rang (des lignes gardées en tête), puis les intitulés de
|
|
1603
|
+
* groupe et le pied, calculés sur ce qui est AFFICHÉ. */
|
|
1604
|
+
const lignesTriees = table.getRowModel().rows
|
|
1605
|
+
const lignesRendues = useMemo(() => {
|
|
1606
|
+
if (!rang) return lignesTriees
|
|
1607
|
+
return lignesTriees
|
|
1608
|
+
.map((r, i) => ({ r, i, k: rang(r.original) }))
|
|
1609
|
+
.sort((a, b) => a.k - b.k || a.i - b.i)
|
|
1610
|
+
.map((x) => x.r)
|
|
1611
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1612
|
+
}, [lignesTriees, rang])
|
|
1613
|
+
const affichees = table.getPrePaginationRowModel().rows.map((r) => r.original)
|
|
1614
|
+
const filtre = columnFilters.length > 0 || globalFilter.trim() !== ''
|
|
1615
|
+
|
|
1616
|
+
const surAffichees = useRef(onLignesAffichees)
|
|
1617
|
+
surAffichees.current = onLignesAffichees
|
|
1618
|
+
const signatureAffichees = `${filtre}|${lignesRendues.map((r) => r.id).join('\u0001')}`
|
|
1619
|
+
useEffect(() => {
|
|
1620
|
+
surAffichees.current?.(lignesRendues.map((r) => r.original), filtre)
|
|
1621
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1622
|
+
}, [signatureAffichees])
|
|
1623
|
+
|
|
1624
|
+
const intercalaire = groupe
|
|
1625
|
+
? (ligne: Row<T>, precedente: Row<T> | undefined) => {
|
|
1626
|
+
const cle = groupe.de(ligne.original)
|
|
1627
|
+
if (precedente && groupe.de(precedente.original) === cle) return null
|
|
1628
|
+
const membres = lignesRendues.filter((r) => groupe.de(r.original) === cle).map((r) => r.original)
|
|
1629
|
+
return (
|
|
1630
|
+
<div className="zv-row zv-row--groupe">
|
|
1631
|
+
{groupe.rendu ? groupe.rendu(cle, membres) : <Mono ton="label">{cle}</Mono>}
|
|
1632
|
+
</div>
|
|
1633
|
+
)
|
|
1634
|
+
}
|
|
1635
|
+
: undefined
|
|
1374
1636
|
|
|
1375
|
-
const
|
|
1637
|
+
const casesPied = pied && lignesRendues.length > 0 ? pied(affichees) : null
|
|
1638
|
+
const total = affichees.length
|
|
1376
1639
|
const { pageIndex: page, pageSize } = table.getState().pagination
|
|
1377
1640
|
const debut = total === 0 ? 0 : page * pageSize + 1
|
|
1378
1641
|
const fin = Math.min(total, (page + 1) * pageSize)
|
|
1379
|
-
const nbChoisies = Object.keys(
|
|
1380
|
-
|
|
1642
|
+
const nbChoisies = externe ? externe.choisies.size : Object.keys(selectionInterne).length
|
|
1643
|
+
|
|
1644
|
+
const tableau = (
|
|
1645
|
+
<DndContext
|
|
1646
|
+
id={idDnd}
|
|
1647
|
+
sensors={capteurs}
|
|
1648
|
+
collisionDetection={closestCenter}
|
|
1649
|
+
modifiers={[axeHorizontal]}
|
|
1650
|
+
onDragEnd={finDeplacement}
|
|
1651
|
+
accessibility={{
|
|
1652
|
+
announcements: annonces,
|
|
1653
|
+
screenReaderInstructions: {
|
|
1654
|
+
draggable: t.dndInstructions,
|
|
1655
|
+
},
|
|
1656
|
+
}}
|
|
1657
|
+
>
|
|
1658
|
+
<SortableContext items={deplacables} strategy={horizontalListSortingStrategy}>
|
|
1659
|
+
<DataTable
|
|
1660
|
+
table={table}
|
|
1661
|
+
className="zv-table--grille"
|
|
1662
|
+
celluleEntete={(p) => <EnteteColonne {...p} t={t} />}
|
|
1663
|
+
sousEntete={rangeeFiltres}
|
|
1664
|
+
onRowClick={onRowClick}
|
|
1665
|
+
ligneActive={ligneActive}
|
|
1666
|
+
onRowContextMenu={onRowContextMenu}
|
|
1667
|
+
onRowDoubleClick={onRowDoubleClick}
|
|
1668
|
+
onRowDragStart={onRowDragStart}
|
|
1669
|
+
lignes={rang ? lignesRendues : undefined}
|
|
1670
|
+
intercalaire={intercalaire}
|
|
1671
|
+
avantLignes={
|
|
1672
|
+
ligneSaisie ? rangee('zv-row zv-row--saisie', (c) => ligneSaisie[c.id] ?? null) : undefined
|
|
1673
|
+
}
|
|
1674
|
+
apresLignes={
|
|
1675
|
+
<>
|
|
1676
|
+
{casesPied && rangee('zv-row zv-row--pied', (c) => casesPied[c.id] ?? null)}
|
|
1677
|
+
{apresLignes}
|
|
1678
|
+
</>
|
|
1679
|
+
}
|
|
1680
|
+
dense={dense}
|
|
1681
|
+
remplir={remplir}
|
|
1682
|
+
vide={
|
|
1683
|
+
vide && !filtre ? (
|
|
1684
|
+
vide
|
|
1685
|
+
) : (
|
|
1686
|
+
<Empty>
|
|
1687
|
+
{data.length === 0 ? t.aucuneDonnee : filtre ? t.aucunResultat : t.aucuneLigne}{' '}
|
|
1688
|
+
{filtre && (
|
|
1689
|
+
<Button variante="fantome" compact onClick={effacerFiltres}>
|
|
1690
|
+
{t.effacerFiltres}
|
|
1691
|
+
</Button>
|
|
1692
|
+
)}
|
|
1693
|
+
</Empty>
|
|
1694
|
+
)
|
|
1695
|
+
}
|
|
1696
|
+
/>
|
|
1697
|
+
</SortableContext>
|
|
1698
|
+
</DndContext>
|
|
1699
|
+
)
|
|
1381
1700
|
|
|
1382
1701
|
return (
|
|
1383
|
-
<div className={cx('zv-grille', className)}>
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1702
|
+
<div className={cx('zv-grille', remplir && 'zv-grille--remplir', className)}>
|
|
1703
|
+
{barre && (
|
|
1704
|
+
<div className="zv-grille__outils">
|
|
1705
|
+
{champRecherche ??
|
|
1706
|
+
(recherche !== false ? (
|
|
1707
|
+
<Search
|
|
1708
|
+
value={globalFilter}
|
|
1709
|
+
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
1710
|
+
placeholder={typeof recherche === 'string' ? recherche : t.rechercher}
|
|
1711
|
+
aria-label={t.rechercher}
|
|
1712
|
+
/>
|
|
1713
|
+
) : (
|
|
1714
|
+
<span />
|
|
1715
|
+
))}
|
|
1716
|
+
<div className="zv-grille__actions">
|
|
1717
|
+
{avecCases && nbChoisies > 0 && <Meta>{t.choisies(nbChoisies)}</Meta>}
|
|
1718
|
+
{outils}
|
|
1719
|
+
{!onVisibilite && <MenuColonnes table={table} t={t} />}
|
|
1720
|
+
{exportCsv !== false && (
|
|
1721
|
+
<Button
|
|
1722
|
+
variante="fantome"
|
|
1723
|
+
compact
|
|
1724
|
+
icone="telecharger"
|
|
1725
|
+
onClick={() => telecharger(versCsv(table), typeof exportCsv === 'string' ? exportCsv : tableId)}
|
|
1726
|
+
>
|
|
1727
|
+
{t.exporter}
|
|
1728
|
+
</Button>
|
|
1729
|
+
)}
|
|
1730
|
+
<Button variante="fantome" compact icone="rembobiner" onClick={reinitialiser}>
|
|
1731
|
+
{t.reinitialiser}
|
|
1409
1732
|
</Button>
|
|
1410
|
-
|
|
1411
|
-
<Button variante="fantome" compact icone="rembobiner" onClick={reinitialiser}>
|
|
1412
|
-
{t.reinitialiser}
|
|
1413
|
-
</Button>
|
|
1733
|
+
</div>
|
|
1414
1734
|
</div>
|
|
1415
|
-
|
|
1735
|
+
)}
|
|
1416
1736
|
|
|
1417
|
-
|
|
1418
|
-
id={idDnd}
|
|
1419
|
-
sensors={capteurs}
|
|
1420
|
-
collisionDetection={closestCenter}
|
|
1421
|
-
modifiers={[axeHorizontal]}
|
|
1422
|
-
onDragEnd={finDeplacement}
|
|
1423
|
-
accessibility={{
|
|
1424
|
-
announcements: annonces,
|
|
1425
|
-
screenReaderInstructions: {
|
|
1426
|
-
draggable: t.dndInstructions,
|
|
1427
|
-
},
|
|
1428
|
-
}}
|
|
1429
|
-
>
|
|
1430
|
-
<SortableContext items={deplacables} strategy={horizontalListSortingStrategy}>
|
|
1431
|
-
<DataTable
|
|
1432
|
-
table={table}
|
|
1433
|
-
className="zv-table--grille"
|
|
1434
|
-
celluleEntete={(p) => <EnteteColonne {...p} t={t} />}
|
|
1435
|
-
sousEntete={rangeeFiltres}
|
|
1436
|
-
onRowClick={onRowClick}
|
|
1437
|
-
ligneActive={ligneActive}
|
|
1438
|
-
vide={
|
|
1439
|
-
vide ?? (
|
|
1440
|
-
<Empty>
|
|
1441
|
-
{data.length === 0 ? t.aucuneDonnee : filtre ? t.aucunResultat : t.aucuneLigne}
|
|
1442
|
-
</Empty>
|
|
1443
|
-
)
|
|
1444
|
-
}
|
|
1445
|
-
/>
|
|
1446
|
-
</SortableContext>
|
|
1447
|
-
</DndContext>
|
|
1737
|
+
{envelopperTableau ? envelopperTableau(tableau) : tableau}
|
|
1448
1738
|
|
|
1449
1739
|
{parPage !== false && (
|
|
1450
1740
|
<Pager pied aria-label={t.pagination}>
|