@benjosivo/table-query 1.0.3 → 1.2.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/README.md CHANGED
@@ -1,215 +1,431 @@
1
- # @benjosivo/table-query
2
-
3
- Tableau de données triable, filtrable et paginé, en deux parties indépendantes :
4
-
5
- - **`@benjosivo/table-query/server`** — logique de requêtage côté serveur (Express + MySQL) : pagination, tri, filtres, calcul des valeurs disponibles pour chaque colonne, mise en cache optionnelle.
6
- - **`@benjosivo/table-query/react`** — composants et hook React pour afficher ce type de données : tableau, pagination, filtre rapide par colonne, panneau de filtres avancés (multiselect / plage numérique / plage de dates).
7
-
8
- Les deux parties fonctionnent ensemble mais peuvent aussi être utilisées séparément (par exemple le composant React seul, avec ton propre backend).
9
-
10
- ## Installation
11
-
12
- ```bash
13
- npm install @benjosivo/table-query
14
- ```
15
-
16
- Selon la partie utilisée, il faut aussi avoir installé dans le projet :
17
-
18
- | Partie utilisée | Dépendances nécessaires |
19
- |---|---|
20
- | `/server` | `express`, `@benjosivo/mysql` |
21
- | `/react` | `react` (v18+) |
22
-
23
- Ce sont des `peerDependencies` : le package ne les installe pas lui-même, il utilise celles déjà présentes dans ton projet.
24
-
25
- ---
26
-
27
- ## Côté serveur
28
-
29
- ### Créer le module
30
-
31
- ```ts
32
- import { createTableQueryModule } from '@benjosivo/table-query/server';
33
-
34
- const { router, reqTableQuery } = createTableQueryModule({
35
- // Formate une date/heure au format attendu par MySQL
36
- convertToMySQLDateTime: (date) => /* ta fonction */,
37
-
38
- // Enveloppe un handler Express (gestion d'erreurs, etc.)
39
- wrapRouteHandler: (fn) => /* ta fonction */,
40
-
41
- // Optionnel : uniquement nécessaire si tu veux pouvoir utiliser le cache (voir plus bas)
42
- cache: {
43
- getSQLCache: (key) => /* lit une entrée de cache */,
44
- setSQLCache: (key, value) => /* écrit une entrée de cache */,
45
- deleteSQLCache: (key) => /* supprime une entrée de cache */,
46
- },
47
- });
48
- ```
49
-
50
- `router` expose la route `GET /getFiltres`, utilisée en interne pour récupérer les filtres calculés en tâche de fond. Monte-le sur le chemin de ton choix :
51
-
52
- ```ts
53
- app.use('/tableCreation/api', router);
54
- ```
55
-
56
- ### Répondre à une requête de table
57
-
58
- ```ts
59
- app.post('/api/commandes', wrapRouteHandler(async (req, res) => {
60
- const result = await reqTableQuery({
61
- query: `SELECT c.*, COUNT(*) OVER() AS TotalCount FROM commandes c`,
62
- req,
63
- // Un type de filtre par colonne renvoyée par la requête (sauf la 1ère, l'identifiant)
64
- paramFilter: ['MULTISELECT', 'HIDE', 'DATE', 'MULTISELECT'],
65
- });
66
-
67
- if (result.error) return res.status(result.status ?? 500).send(result.error);
68
- res.json(result.data);
69
- }));
70
- ```
71
-
72
- `reqTableQuery` lit `limit`, `offset`, `sorting`, `filtre` et `setFilter` dans `req.body` (ou `req.query`) — c'est exactement ce que le composant React `DataTable`/`useDataTable` envoie, donc les deux côtés s'emboîtent directement.
73
-
74
- ### Types de filtre disponibles (`paramFilter`)
75
-
76
- | Type | Effet |
77
- |---|---|
78
- | `'MULTISELECT'` | Liste de valeurs distinctes de la colonne, sélection multiple |
79
- | `'UNGROUP_MULTISELECT'` | Comme `MULTISELECT`, mais éclate les valeurs séparées par `, ` dans une même cellule |
80
- | `'SLIDER'` | Plage numérique (min/max) |
81
- | `'DATE'` / `'DATETIME'` | Plage de dates |
82
- | `'JSON'` / `'FILE'` | Pas de filtre, juste un rendu spécifique côté React |
83
- | `'HIDE'` | Colonne sans filtre |
84
- | `null` | Colonne sans filtre particulier |
85
-
86
- ### Le cache (`useCache`)
87
-
88
- Le cache est activé **par appel**, pas globalement :
89
-
90
- ```ts
91
- reqTableQuery({ query, req, paramFilter, useCache: true }); // utilise le cache
92
- reqTableQuery({ query, req, paramFilter, useCache: false }); // toujours une donnée fraîche
93
- ```
94
-
95
- - Si `cache` a été fourni à `createTableQueryModule`, `useCache` vaut `true` par défaut.
96
- - Si `cache` n'a pas été fourni, `useCache` vaut `false` par défaut (aucun Redis requis).
97
- - Demander `useCache: true` sans avoir fourni `cache` lève une erreur explicite.
98
- - Avec `useCache: false`, les valeurs de filtre disponibles sont calculées et renvoyées directement dans la réponse. Avec `useCache: true`, elles sont calculées en tâche de fond et le composant React va les chercher via `/getFiltres` (polling automatique, rien à faire côté appelant).
99
-
100
- ---
101
-
102
- ## Côté React
103
-
104
- ### Utilisation simple (tout inclus)
105
-
106
- ```tsx
107
- import { DataTable } from '@benjosivo/table-query/react';
108
-
109
- function CommandesTable() {
110
- return (
111
- <DataTable
112
- fetchData={(params) =>
113
- fetch('/api/commandes', {
114
- method: 'POST',
115
- headers: { 'Content-Type': 'application/json' },
116
- body: JSON.stringify(params),
117
- }).then((res) => (res.ok ? res.json() : null))
118
- }
119
- onRowClick={(id, row) => console.log('ligne cliquée', id, row)}
120
- advancedFilters
121
- />
122
- );
123
- }
124
- ```
125
-
126
- #### Props de `<DataTable />`
127
-
128
- | Prop | Type | Description |
129
- |---|---|---|
130
- | `fetchData` | `(params) => Promise<FetchResult \| null>` | **Obligatoire.** Appelée à chaque changement de page/tri/filtre. |
131
- | `onRowClick` | `(id, row) => void` | Appelée au clic sur une ligne |
132
- | `filterEnabled` | `boolean` (défaut `true`) | Affiche le bouton de filtre rapide par colonne |
133
- | `sortingEnabled` | `boolean` (défaut `true`) | Active le tri au clic sur l'en-tête |
134
- | `advancedFilters` | `boolean` (défaut `false`) | Affiche le panneau de filtres avancés (nécessite `setFilter`/`paramFilter` côté serveur) |
135
- | `height` | `string` (défaut `'76vh'`) | Hauteur max de la zone de défilement |
136
- | `rowsPerPageOptions` | `number[]` | Choix disponibles pour le nombre de lignes par page |
137
- | `defaultRowsPerPage` | `number` | Valeur par défaut |
138
- | `onImagePreview` | `(src) => void` | Appelée au clic sur une image (sinon ouverture dans un nouvel onglet) |
139
-
140
- ### Utilisation avec ta propre UI de filtre
141
-
142
- Si le système de filtre par défaut ne convient pas, utilise le hook et les composants séparément :
143
-
144
- ```tsx
145
- import { useDataTable, Table, Pagination } from '@benjosivo/table-query/react';
146
-
147
- function CommandesTable() {
148
- const table = useDataTable({ fetchData: fetchCommandes });
149
- const [recherche, setRecherche] = useState('');
150
-
151
- const rechercher = () => {
152
- table.replaceFilters({ reference: [`/*/${recherche}/*/`] });
153
- // ou, filtre par filtre : table.setColumnFilter('statut', ['en_cours']);
154
- };
155
-
156
- return (
157
- <>
158
- <input value={recherche} onChange={(e) => setRecherche(e.target.value)} />
159
- <button onClick={rechercher}>Rechercher</button>
160
-
161
- <Table
162
- items={table.items}
163
- fieldsType={table.fieldsType}
164
- sortColumn={table.sortColumn}
165
- sortDirection={table.sortDirection}
166
- onSort={table.toggleSort}
167
- />
168
-
169
- <Pagination
170
- page={table.page}
171
- totalPages={table.totalPages}
172
- perPage={table.perPage}
173
- count={table.count}
174
- onChangePerPage={table.changePerPage}
175
- onFirst={table.firstPage}
176
- onPrevious={table.previousPage}
177
- onNext={table.nextPage}
178
- onLast={table.lastPage}
179
- />
180
- </>
181
- );
182
- }
183
- ```
184
-
185
- `useDataTable` gère la pagination, le tri et l'appel réseau ; il n'impose aucune UI de filtre — `table.setColumnFilter(colonne, valeur)` et `table.replaceFilters(objet)` permettent de piloter le filtrage depuis n'importe quelle interface.
186
-
187
- ### Format attendu par `fetchData`
188
-
189
- ```ts
190
- interface FetchParams {
191
- limit: number;
192
- offset: number;
193
- sorting?: string;
194
- filtre?: Record<string, unknown>;
195
- setFilter?: boolean;
196
- }
197
-
198
- interface FetchResult<T> {
199
- items: T[];
200
- count: number;
201
- fieldsType?: { fieldType: string }[]; // 'DATE' | 'DATETIME' | 'JSON' | 'FILE' | 'BLOB' | ...
202
- filtre?: FilterConfig; // renvoyé par le serveur si des filtres avancés sont demandés
203
- cleRecupFiltre?: string; // utilisé en interne pour le polling du cache
204
- }
205
- ```
206
-
207
- C'est exactement la forme renvoyée par `reqTableQuery` côté serveur.
208
-
209
- ---
210
-
211
- ## Build
212
-
213
- ```bash
214
- npm run build # compile src/react et src/server dans dist/
1
+ # @benjosivo/table-query
2
+
3
+ Tableau de données triable, filtrable et paginé, en deux parties indépendantes :
4
+
5
+ - **`@benjosivo/table-query/server`** — logique de requêtage côté serveur (Express + MySQL) : pagination, tri, filtres, calcul des valeurs disponibles pour chaque colonne, mise en cache optionnelle.
6
+ - **`@benjosivo/table-query/react`** — composants et hook React pour afficher ce type de données : tableau, pagination, filtre rapide par colonne, panneau de filtres avancés (multiselect / plage numérique / plage de dates).
7
+
8
+ Les deux parties fonctionnent ensemble mais peuvent aussi être utilisées séparément (par exemple le composant React seul, avec ton propre backend).
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @benjosivo/table-query
14
+ ```
15
+
16
+ Selon la partie utilisée, il faut aussi avoir installé dans le projet :
17
+
18
+ | Partie utilisée | Dépendances nécessaires |
19
+ |---|---|
20
+ | `/server` | `express`, `@benjosivo/mysql` |
21
+ | `/react` | `react` (v18+) |
22
+
23
+ Ce sont des `peerDependencies` : le package ne les installe pas lui-même, il utilise celles déjà présentes dans ton projet.
24
+
25
+ ---
26
+
27
+ ## Côté serveur
28
+
29
+ ### Créer le module
30
+
31
+ ```ts
32
+ import { createTableQueryModule } from '@benjosivo/table-query/server';
33
+
34
+ const { router, reqTableQuery } = createTableQueryModule({
35
+ // Formate une date/heure au format attendu par MySQL
36
+ convertToMySQLDateTime: (date) => /* ta fonction */,
37
+
38
+ // Enveloppe un handler Express (gestion d'erreurs, etc.)
39
+ wrapRouteHandler: (fn) => /* ta fonction */,
40
+
41
+ // Optionnel : uniquement nécessaire si tu veux pouvoir utiliser le cache (voir plus bas)
42
+ cache: {
43
+ getSQLCache: (key) => /* lit une entrée de cache */,
44
+ setSQLCache: (key, value) => /* écrit une entrée de cache */,
45
+ deleteSQLCache: (key) => /* supprime une entrée de cache */,
46
+ },
47
+ });
48
+ ```
49
+
50
+ `router` expose la route `GET /getFiltres`, utilisée en interne pour récupérer les filtres calculés en tâche de fond. Monte-le sur le chemin de ton choix :
51
+
52
+ ```ts
53
+ app.use('/tableCreation/api', router);
54
+ ```
55
+
56
+ ### Répondre à une requête de table
57
+
58
+ ```ts
59
+ app.post('/api/commandes', wrapRouteHandler(async (req, res) => {
60
+ const result = await reqTableQuery({
61
+ query: `SELECT c.*, COUNT(*) OVER() AS TotalCount FROM commandes c`,
62
+ req,
63
+ // Un type de filtre par colonne renvoyée par la requête (sauf la 1ère, l'identifiant)
64
+ paramFilter: ['MULTISELECT', 'HIDE', 'DATE', 'MULTISELECT'],
65
+ });
66
+
67
+ if (result.error) return res.status(result.status ?? 500).send(result.error);
68
+ res.json(result.data);
69
+ }));
70
+ ```
71
+
72
+ `reqTableQuery` lit `limit`, `offset`, `sorting`, `filtre` et `setFilter` dans `req.body` (ou `req.query`) — c'est exactement ce que le composant React `DataTable`/`useDataTable` envoie, donc les deux côtés s'emboîtent directement.
73
+
74
+ ### Types de filtre disponibles (`paramFilter`)
75
+
76
+ | Type | Effet |
77
+ |---|---|
78
+ | `'MULTISELECT'` | Liste de valeurs distinctes de la colonne, sélection multiple |
79
+ | `'UNGROUP_MULTISELECT'` | Comme `MULTISELECT`, mais éclate les valeurs séparées par `, ` dans une même cellule |
80
+ | `'SLIDER'` | Plage numérique (min/max) |
81
+ | `'DATE'` / `'DATETIME'` | Plage de dates |
82
+ | `'JSON'` / `'FILE'` | Pas de filtre, juste un rendu spécifique côté React |
83
+ | `'HIDE'` | Colonne sans filtre |
84
+ | `null` | Colonne sans filtre particulier |
85
+
86
+ ### Règles de mise en forme (`formattingRules`)
87
+
88
+ `reqTableQuery` peut transporter des règles de couleur jusqu'au client. Elles sont renvoyées **telles quelles** dans `payload.formattingRules` : le serveur ne les évalue jamais et elles ne touchent **jamais** au SQL.
89
+
90
+ ```js
91
+ const { data } = await reqTableQuery({
92
+ query: `SELECT ...`,
93
+ req,
94
+ paramFilter: [null, 'MULTISELECT', 'SLIDER'],
95
+ formattingRules: [
96
+ { column: 'statut', operator: '=', value: 'en retard', style: { backgroundColor: '#ffd7d7' } },
97
+ { column: 'montant', operator: '>', value: 10000, target: 'cell', style: { fontWeight: 'bold' } },
98
+ ],
99
+ });
100
+ ```
101
+
102
+ Les règles sont indexées **par nom de colonne** (contrairement à `paramFilter`, qui est positionnel). Une règle dont la `column` ne correspond à aucune colonne de la requête est ignorée, avec un `console.warn` — jamais une erreur.
103
+
104
+ ### Le cache (`useCache`)
105
+
106
+ Le cache est activé **par appel**, pas globalement :
107
+
108
+ ```ts
109
+ reqTableQuery({ query, req, paramFilter, useCache: true }); // utilise le cache
110
+ reqTableQuery({ query, req, paramFilter, useCache: false }); // toujours une donnée fraîche
111
+ ```
112
+
113
+ - Si `cache` a été fourni à `createTableQueryModule`, `useCache` vaut `true` par défaut.
114
+ - Si `cache` n'a pas été fourni, `useCache` vaut `false` par défaut (aucun Redis requis).
115
+ - Demander `useCache: true` sans avoir fourni `cache` lève une erreur explicite.
116
+ - Avec `useCache: false`, les valeurs de filtre disponibles sont calculées et renvoyées directement dans la réponse. Avec `useCache: true`, elles sont calculées en tâche de fond et le composant React va les chercher via `/getFiltres` (polling automatique, rien à faire côté appelant).
117
+
118
+ ---
119
+
120
+ ## Côté React
121
+
122
+ ### Utilisation simple (tout inclus)
123
+
124
+ ```tsx
125
+ import { DataTable } from '@benjosivo/table-query/react';
126
+
127
+ function CommandesTable() {
128
+ return (
129
+ <DataTable
130
+ fetchData={(params) =>
131
+ fetch('/api/commandes', {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify(params),
135
+ }).then((res) => (res.ok ? res.json() : null))
136
+ }
137
+ onRowClick={(id, row) => console.log('ligne cliquée', id, row)}
138
+ advancedFilters
139
+ />
140
+ );
141
+ }
142
+ ```
143
+
144
+ #### Props de `<DataTable />`
145
+
146
+ | Prop | Type | Description |
147
+ |---|---|---|
148
+ | `fetchData` | `(params) => Promise<FetchResult \| null>` | **Obligatoire.** Appelée à chaque changement de page/tri/filtre. |
149
+ | `onRowClick` | `(id, row) => void` | Appelée au clic sur une ligne |
150
+ | `filterEnabled` | `boolean` (défaut `true`) | Affiche le bouton de filtre rapide par colonne |
151
+ | `sortingEnabled` | `boolean` (défaut `true`) | Active le tri au clic sur l'en-tête |
152
+ | `advancedFilters` | `boolean` (défaut `false`) | Affiche le panneau de filtres avancés (nécessite `setFilter`/`paramFilter` côté serveur) |
153
+ | `height` | `string` (défaut `'76vh'`) | Hauteur max de la zone de défilement |
154
+ | `rowsPerPageOptions` | `number[]` | Choix disponibles pour le nombre de lignes par page |
155
+ | `defaultRowsPerPage` | `number` | Valeur par défaut |
156
+ | `onImagePreview` | `(src) => void` | Appelée au clic sur une image (sinon ouverture dans un nouvel onglet) |
157
+ | `selectable` | `boolean` (défaut `false`) | Affiche une colonne de checkbox (voir « Sélection de lignes ») |
158
+ | `selectionColumnPosition` | `number \| 'start' \| 'end'` (défaut `'start'`) | Position de la colonne de checkbox parmi les colonnes visibles |
159
+ | `selectedIds` | `any[]` | Sélection contrôlée par le parent (voir plus bas) |
160
+ | `onSelectionChange` | `(ids, rows) => void` | Appelée à chaque changement de sélection, avec les ids et les lignes complètes |
161
+ | `formattingRules` | `FormattingRule[]` | Règles de mise en forme conditionnelle (voir « Mise en forme conditionnelle ») |
162
+ | `getRowFormatting` | `(row, i) => {style?, className?}` | Échappatoire : style de ligne calculé en JS, appliqué après toutes les règles |
163
+ | `getCellFormatting` | `(col, value, row, i) => {style?, className?}` | Idem, par cellule |
164
+ | `formattingEditor` | `boolean` (défaut `false`) | Affiche le bouton « Mise en forme » pour l'utilisateur final |
165
+ | `formattingStorageKey` | `string` | Sauvegarde les règles de l'utilisateur dans `localStorage` sous cette clé |
166
+ | `onFormattingRulesChange` | `(rules) => void` | Appelée à chaque modification des règles de l'utilisateur |
167
+ | `initialUserFormattingRules` | `FormattingRule[]` | Réhydrate les règles utilisateur depuis ton backend (prioritaire sur `localStorage`) |
168
+ | `formattingButtonLabel` | `string` (défaut `'Mise en forme'`) | Libellé du bouton de la barre d'outils |
169
+
170
+ ### Sélection de lignes (checkbox)
171
+
172
+ Avec `selectable`, une colonne de checkbox est ajoutée. La checkbox de l'en-tête coche/décoche **toutes les lignes affichées** (la page courante) et passe en état indéterminé quand seule une partie l'est.
173
+
174
+ ```tsx
175
+ const [selection, setSelection] = useState<any[]>([]);
176
+
177
+ <DataTable
178
+ fetchData={fetchCommandes}
179
+ selectable
180
+ selectionColumnPosition='start' // 'start' | 'end' | index (ex. 2)
181
+ onSelectionChange={(ids, rows) => {
182
+ setSelection(ids); // ids = valeur de la 1ère colonne de chaque ligne
183
+ console.log(rows); // les lignes entières, comme dans onRowClick
184
+ }}
185
+ />
186
+ ```
187
+
188
+ - **Identifiant d'une ligne** : la valeur de sa **première colonne** — la même règle que `onRowClick`. La colonne peut être masquée (`HIDE`), l'identifiant reste utilisable.
189
+ - **Position** : `'start'` (défaut), `'end'`, ou un index 0-based **parmi les colonnes visibles** (`2` = 3ᵉ colonne ; les colonnes `HIDE` ne comptent pas). Un index hors limites est ramené au début ou à la fin.
190
+ - **Entre les pages** : la sélection est cumulative — on peut cocher des lignes page 1, aller page 2, et `onSelectionChange` renvoie l'ensemble (ids **et** lignes complètes, même celles qui ne sont plus affichées).
191
+ - **Changement de tri ou de filtre** : le jeu de données n'est plus le même, la sélection est donc vidée et `onSelectionChange([], [])` est appelée. Changer de page ou le nombre de lignes par page ne la vide pas.
192
+ - Cliquer une checkbox ne déclenche pas `onRowClick`.
193
+
194
+ #### Sélection contrôlée
195
+
196
+ Si le parent passe `selectedIds`, c'est lui qui détient l'état : le tableau n'affiche que ce qu'on lui donne. Pratique pour cocher des lignes par programme ou vider la sélection après une action groupée.
197
+
198
+ ```tsx
199
+ const [selection, setSelection] = useState<any[]>([]);
200
+
201
+ const supprimer = async () => {
202
+ await fetch('/api/commandes/suppression', { method: 'POST', body: JSON.stringify({ ids: selection }) });
203
+ setSelection([]); // on vide la sélection nous-mêmes
204
+ };
205
+
206
+ <DataTable
207
+ fetchData={fetchCommandes}
208
+ selectable
209
+ selectedIds={selection}
210
+ onSelectionChange={(ids) => setSelection(ids)}
211
+ />
212
+ ```
213
+
214
+ ### Mise en forme conditionnelle (couleurs)
215
+
216
+ Colorer des lignes ou des cellules selon leurs valeurs, façon Excel. Les règles sont des objets **sérialisables** : elles peuvent être écrites en dur, stockées en base, ou renvoyées par l'API.
217
+
218
+ ```tsx
219
+ <DataTable
220
+ fetchData={fetchCommandes}
221
+ formattingRules={[
222
+ // Toute la ligne en rouge pâle quand le statut vaut "en retard"
223
+ { column: 'statut', operator: '=', value: 'en retard', style: { backgroundColor: '#ffd7d7' } },
224
+ // Seulement la cellule "montant" en gras vert au-dessus de 10 000
225
+ { column: 'montant', operator: '>', value: 10000, target: 'cell', style: { color: '#0a7d32', fontWeight: 'bold' } },
226
+ // Deux colonnes précises, via une classe CSS de ton app
227
+ { column: 'livraison', operator: 'isNull', target: ['livraison', 'transporteur'], className: 'a-completer' },
228
+ ]}
229
+ />
230
+ ```
231
+
232
+ La librairie ne livre **aucun CSS** : `style` (inline) fonctionne sans configuration, `className` suppose que ton app définit la classe.
233
+
234
+ #### Écrire une règle (`FormattingRule`)
235
+
236
+ | Champ | Type | Description |
237
+ |---|---|---|
238
+ | `column` | `string` | **Obligatoire.** Nom de la colonne testée (une clé des objets de `items`) |
239
+ | `operator` | voir table ci-dessous | **Obligatoire.** |
240
+ | `value` | `any` | L'opérande ; sa forme dépend de l'opérateur |
241
+ | `target` | `'row' \| 'cell' \| string[]` | Ce qui est coloré. Défaut `'row'` |
242
+ | `style` | `CSSProperties` | Style inline appliqué au `<tr>` ou au `<td>` |
243
+ | `className` | `string` | Classe CSS ajoutée au `<tr>` ou au `<td>` |
244
+ | `valueType` | `'auto' \| 'string' \| 'number' \| 'date' \| 'boolean'` | Force le mode de comparaison. Défaut `'auto'` |
245
+ | `stopIfTrue` | `boolean` | Arrête les règles suivantes sur ce que cette règle a coloré |
246
+ | `enabled` | `boolean` | `false` conserve la règle sans l'appliquer. Défaut `true` |
247
+ | `label` | `string` | Libellé affiché dans l'éditeur |
248
+ | `id` | `string` | Généré automatiquement si absent |
249
+
250
+ | Opérateur | Opérande |
251
+ |---|---|
252
+ | `=` `!=` `<` `<=` `>` `>=` | une valeur |
253
+ | `between` | `[min, max]` ou `{min, max}` — **bornes incluses**, inversées tolérées |
254
+ | `in` | un tableau, ou une chaîne `"a, b, c"` |
255
+ | `contains` `notContains` `startsWith` `endsWith` | une valeur (toujours comparée en texte) |
256
+ | `isNull` `isNotNull` | aucune |
257
+
258
+ #### Cible d'une règle (`target`)
259
+
260
+ - `'row'` (défaut) → le `<tr>` entier.
261
+ - `'cell'` → seulement la cellule de la colonne testée.
262
+ - `['col_a', 'col_b']` → ces colonnes-là.
263
+
264
+ Le style de ligne est **aussi** posé sur chaque `<td>`. Sans cela, le moindre CSS de ton app sur `td` (zébrage, `tbody td { background: #fff }`) recouvrirait le fond du `<tr>` et la couleur semblerait ne pas marcher. Une règle `'cell'` est étalée **par-dessus**, elle l'emporte donc propriété par propriété.
265
+
266
+ Les `className`, eux, ne descendent **pas** sur les `<td>` : une classe de ligne est un point d'accroche pour ton propre CSS, écris `tr.ma-classe td { ... }`.
267
+
268
+ #### Ordre, fusion et `stopIfTrue`
269
+
270
+ Les règles sont évaluées **dans l'ordre**, et cet ordre **est** la priorité :
271
+
272
+ 1. `formattingRules` (props de ton app)
273
+ 2. les règles renvoyées par l'API
274
+ 3. les règles créées par l'utilisateur dans l'éditeur
275
+
276
+ Les styles se **cumulent** ; sur une même propriété CSS, **la dernière règle gagne**. Deux règles peuvent donc apporter l'une le fond, l'autre le gras.
277
+
278
+ `stopIfTrue` gèle exactement ce que la règle a coloré : posé sur une règle `'row'`, il bloque les règles `'row'` suivantes mais **pas** les règles `'cell'` ; posé sur une règle `'cell'`, il ne gèle que cette cellule.
279
+
280
+ #### Comparaison des valeurs
281
+
282
+ Les valeurs viennent de MySQL : un `DECIMAL` arrive en chaîne, une `DATE` en objet `Date`. En mode `'auto'`, la comparaison essaie dans cet ordre : **date** (si un `Date` est en jeu) → **nombre** (si les deux côtés sont numériques) → **date ISO** → **texte**.
283
+
284
+ - Le texte est comparé **sans tenir compte de la casse**.
285
+ - `'007'` et `'7'` sont **égaux** en mode auto (comparaison numérique). Pour une référence ou un code postal, mets `valueType: 'string'`.
286
+ - `'2024'` est traité comme un **nombre**, pas comme une année.
287
+ - Une valeur `'YYYY-MM-DD'` désigne le **jour entier** : `= '2024-01-05'` matche un `DATETIME` du 5 à 14h32, et `between` inclut toute la journée de fin.
288
+
289
+ **Valeurs nulles** : `isNull` matche `null`, `undefined` et la chaîne vide ; `!=` et `notContains` matchent sur une valeur nulle ; **tous les autres opérateurs ne matchent jamais** sur `null`. Une règle visant une colonne **inexistante** ne matche rien du tout (y compris `isNull`).
290
+
291
+ #### Échappatoire : `getRowFormatting` / `getCellFormatting`
292
+
293
+ Pour une logique qui croise plusieurs colonnes, hors de portée d'une règle déclarative :
294
+
295
+ ```tsx
296
+ const getRowFormatting = useCallback(
297
+ (row) => (row.livree > row.commandee ? { style: { backgroundColor: '#ffe6e6' } } : null),
298
+ [],
299
+ );
300
+
301
+ <DataTable fetchData={fetchCommandes} getRowFormatting={getRowFormatting} />
302
+ ```
303
+
304
+ Ces callbacks sont appliqués **en dernier** et ignorent `stopIfTrue` — ils l'emportent toujours. **Enveloppe-les dans `useCallback`**, sinon le calcul des couleurs est refait à chaque rendu.
305
+
306
+ #### L'éditeur pour l'utilisateur final (`formattingEditor`)
307
+
308
+ Désactivé par défaut. Avec `formattingEditor`, un bouton « Mise en forme » apparaît au-dessus du tableau et ouvre une modale où l'utilisateur ajoute, réordonne, désactive et supprime ses propres règles.
309
+
310
+ ```tsx
311
+ <DataTable
312
+ fetchData={fetchCommandes}
313
+ formattingEditor
314
+ formattingStorageKey='commandes' // persistance locale, gérée par la lib
315
+ onFormattingRulesChange={(rules) => save(rules)} // ou ta propre persistance serveur
316
+ initialUserFormattingRules={reglesDeMonBackend} // prioritaire sur localStorage
317
+ />
318
+ ```
319
+
320
+ - `formattingStorageKey` écrit sous la clé réelle `tableQuery:formatting:<clé>`, au format `{"v":1,"rules":[...],"disabled":[...]}`. Tout accès au stockage est protégé (SSR, navigation privée, quota dépassé) et une version inconnue est ignorée.
321
+ - L'utilisateur n'édite que **sa** couche. Les règles venues des props et de l'API s'affichent en lecture seule, avec une case pour les désactiver.
322
+ - Sans `formattingEditor`, aucun nœud supplémentaire n'est ajouté au DOM.
323
+
324
+ #### Limites connues
325
+
326
+ - Sur une colonne `JSON`, la coloration du texte peut sembler sans effet : le rendu JSON pose ses propres `<span class="key|string|number">`, et le CSS de ton app sur ces classes l'emporte sur la couleur héritée du `<td>`. Le fond, lui, fonctionne.
327
+ - Sur une colonne `FILE`/`BLOB`, le contenu est un `<img>` ou un `<button>` : le fond s'affiche autour, mais la couleur du texte est écrasée par le style de tes boutons.
328
+ - Une règle visant une colonne masquée (`HIDE`) est bien évaluée, mais une cible `'cell'` sur cette colonne n'a aucun effet visible.
329
+
330
+ ### Utilisation avec ta propre UI de filtre
331
+
332
+ Si le système de filtre par défaut ne convient pas, utilise le hook et les composants séparément :
333
+
334
+ ```tsx
335
+ import { useDataTable, Table, Pagination } from '@benjosivo/table-query/react';
336
+
337
+ function CommandesTable() {
338
+ const table = useDataTable({ fetchData: fetchCommandes });
339
+ const [recherche, setRecherche] = useState('');
340
+
341
+ const rechercher = () => {
342
+ table.replaceFilters({ reference: [`/*/${recherche}/*/`] });
343
+ // ou, filtre par filtre : table.setColumnFilter('statut', ['en_cours']);
344
+ };
345
+
346
+ return (
347
+ <>
348
+ <input value={recherche} onChange={(e) => setRecherche(e.target.value)} />
349
+ <button onClick={rechercher}>Rechercher</button>
350
+
351
+ <Table
352
+ items={table.items}
353
+ fieldsType={table.fieldsType}
354
+ sortColumn={table.sortColumn}
355
+ sortDirection={table.sortDirection}
356
+ onSort={table.toggleSort}
357
+ />
358
+
359
+ <Pagination
360
+ page={table.page}
361
+ totalPages={table.totalPages}
362
+ perPage={table.perPage}
363
+ count={table.count}
364
+ onChangePerPage={table.changePerPage}
365
+ onFirst={table.firstPage}
366
+ onPrevious={table.previousPage}
367
+ onNext={table.nextPage}
368
+ onLast={table.lastPage}
369
+ />
370
+ </>
371
+ );
372
+ }
373
+ ```
374
+
375
+ `useDataTable` gère la pagination, le tri et l'appel réseau ; il n'impose aucune UI de filtre — `table.setColumnFilter(colonne, valeur)` et `table.replaceFilters(objet)` permettent de piloter le filtrage depuis n'importe quelle interface.
376
+
377
+ Le hook gère aussi la sélection, que tu utilises `<DataTable />` ou le `<Table />` nu :
378
+
379
+ ```tsx
380
+ const table = useDataTable({ fetchData, onSelectionChange: (ids, rows) => console.log(ids, rows) });
381
+
382
+ <Table
383
+ items={table.items}
384
+ fieldsType={table.fieldsType}
385
+ selectable
386
+ selectionColumnPosition='end'
387
+ selectedIds={table.selectedIds}
388
+ onToggleRow={table.setRowSelected}
389
+ onToggleAllRows={table.setAllRowsSelected}
390
+ />
391
+ ```
392
+
393
+ | Valeur renvoyée | Type | Description |
394
+ |---|---|---|
395
+ | `selectedIds` | `any[]` | Ids sélectionnés (1ère colonne de chaque ligne) |
396
+ | `selectedRows` | `T[]` | Les lignes complètes correspondantes, pages précédentes comprises |
397
+ | `isRowSelected` | `(row) => boolean` | |
398
+ | `setRowSelected` | `(row, selected) => void` | |
399
+ | `toggleRowSelection` | `(row) => void` | |
400
+ | `setAllRowsSelected` | `(selected) => void` | Toutes les lignes affichées ; les autres pages ne sont pas touchées |
401
+ | `clearSelection` | `() => void` | Vide la sélection |
402
+
403
+ ### Format attendu par `fetchData`
404
+
405
+ ```ts
406
+ interface FetchParams {
407
+ limit: number;
408
+ offset: number;
409
+ sorting?: string;
410
+ filtre?: Record<string, unknown>;
411
+ setFilter?: boolean;
412
+ }
413
+
414
+ interface FetchResult<T> {
415
+ items: T[];
416
+ count: number;
417
+ fieldsType?: { fieldType: string }[]; // 'DATE' | 'DATETIME' | 'JSON' | 'FILE' | 'BLOB' | ...
418
+ filtre?: FilterConfig; // renvoyé par le serveur si des filtres avancés sont demandés
419
+ cleRecupFiltre?: string; // utilisé en interne pour le polling du cache
420
+ }
421
+ ```
422
+
423
+ C'est exactement la forme renvoyée par `reqTableQuery` côté serveur.
424
+
425
+ ---
426
+
427
+ ## Build
428
+
429
+ ```bash
430
+ npm run build # compile src/react et src/server dans dist/
215
431
  ```