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