data-primals-engine 1.3.4 → 1.4.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.
Files changed (42) hide show
  1. package/README.md +72 -2
  2. package/client/README.md +20 -0
  3. package/client/package-lock.json +712 -147
  4. package/client/package.json +38 -37
  5. package/client/src/App.jsx +9 -10
  6. package/client/src/App.scss +7 -1
  7. package/client/src/Dashboard.jsx +349 -208
  8. package/client/src/DashboardView.jsx +569 -547
  9. package/client/src/DataEditor.jsx +20 -2
  10. package/client/src/DataLayout.jsx +24 -12
  11. package/client/src/DataTable.jsx +807 -760
  12. package/client/src/DataTable.scss +187 -90
  13. package/client/src/Field.jsx +1783 -1656
  14. package/client/src/ModelCreator.jsx +2 -2
  15. package/client/src/ModelCreatorField.jsx +906 -804
  16. package/client/src/ModelList.jsx +2 -2
  17. package/client/src/PackGallery.jsx +391 -290
  18. package/client/src/PackGallery.scss +231 -210
  19. package/client/src/constants.js +16 -4
  20. package/client/src/translations.js +16750 -16392
  21. package/package.json +14 -11
  22. package/src/core.js +9 -0
  23. package/src/defaultModels.js +18 -10
  24. package/src/email.js +2 -1
  25. package/src/gameObject.js +1 -1
  26. package/src/i18n.js +501 -28
  27. package/src/modules/auth-google/index.js +51 -0
  28. package/src/modules/auth-microsoft/index.js +82 -0
  29. package/src/modules/auth-saml/index.js +90 -0
  30. package/src/modules/data/data.core.js +5 -1
  31. package/src/modules/data/data.history.js +489 -489
  32. package/src/modules/data/data.js +86 -12
  33. package/src/modules/data/data.routes.js +1691 -1663
  34. package/src/modules/user.js +19 -0
  35. package/src/modules/workflow.js +2 -12
  36. package/src/providers.js +110 -1
  37. package/src/sso.js +194 -0
  38. package/test/data.integration.test.js +3 -0
  39. package/test/user.test.js +1 -1
  40. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  41. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  42. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
@@ -1,209 +1,350 @@
1
- import React, {useState, useMemo, useEffect} from 'react';
2
- import { Trans, useTranslation } from 'react-i18next';
3
- import {FaPlus, FaSpinner} from "react-icons/fa"; // Ajout FaTrash
4
-
5
- import "./Dashboard.scss"
6
- import {useMutation, useQuery, useQueryClient} from "react-query";
7
- import { useAuthContext } from "./contexts/AuthContext.jsx";
8
- import { SelectField } from "./Field.jsx";
9
- import {DashboardView} from "./DashboardView.jsx";
10
- import {useModelContext} from "./contexts/ModelContext.jsx";
11
- import {useParams, useSearchParams} from "react-router-dom";
12
- import {getObjectHash} from "../../src/core.js";
13
- import {getUserHash, getUserId} from "../../src/data.js";
14
-
15
- // --- DashboardsPage (Reste inchangé) ---
16
- export function DashboardsPage() {
17
- const { setSelectedModel } = useModelContext()
18
- const { t } = useTranslation();
19
- const { me } = useAuthContext();
20
- const [selectedDashboardId, setSelectedDashboardId] = useState(null);
21
-
22
- const [ searchParams, setSearchParams ] = useSearchParams();
23
- const { hash } = useParams(); // 2. Récupérer le hash de l'URL
24
-
25
- // ... après les autres hooks useState, useQuery, etc.
26
- const queryClient = useQueryClient();
27
- const [newDashboardName, setNewDashboardName] = useState('');
28
- const [isCreating, setIsCreating] = useState(false); // <-- AJOUTEZ CETTE LIGNE
29
-
30
- const createDashboardMutation = useMutation(
31
- async (dashboardName) => {
32
- const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
33
- const response = await fetch('/api/data', {
34
- method: 'POST',
35
- headers: { 'Content-Type': 'application/json' },
36
- body: JSON.stringify({
37
- model: 'dashboard',
38
- data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
39
- })
40
- });
41
- if (!response.ok) {
42
- const errorData = await response.json();
43
- throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
44
- }
45
- return response.json();
46
- },
47
- {
48
- onSuccess: () => {
49
- // Rafraîchit la liste des dashboards après la création
50
- queryClient.invalidateQueries(['userDashboards', me?.username]);
51
- setNewDashboardName('');
52
- setIsCreating(false);
53
- },
54
- onError: (error) => {
55
- // Idéalement, afficher une notification à l'utilisateur
56
- console.error("Erreur lors de la création du dashboard:", error);
57
- }
58
- }
59
- );
60
-
61
- const handleCreateDashboard = (e) => {
62
- e.preventDefault();
63
- if (newDashboardName.trim()) {
64
- createDashboardMutation.mutate(newDashboardName.trim());
65
- }
66
- };
67
- const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
68
- ['userDashboards', me?.username],
69
- async () => {
70
- if (!me?.username) return null;
71
- const response = await fetch(
72
- `/api/data/search?model=dashboard&_user=${me.username}`, {
73
- method: 'POST',
74
- headers: { 'Content-Type': 'application/json' },
75
- // body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
76
- });
77
- if (!response.ok) {
78
- const res = await response.json();
79
- throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
80
- }
81
- return await response.json();
82
- },
83
- {
84
- enabled: !!me?.username
85
- }
86
- );
87
- const [isLoading, setIsLoading] = useState(true);
88
-
89
- // 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
90
- useEffect(() => {
91
- if (dashboardsData?.data.length > 0) {
92
- if( hash ) {
93
- const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
94
- setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
95
- }else{
96
- const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
97
- if( !defaultDashboard) {
98
- setSelectedDashboardId(dashboardsData.data[0]._id);
99
- }else
100
- setSelectedDashboardId(defaultDashboard._id || null);
101
- }
102
- }
103
- setIsLoading(false);
104
- }, [hash,dashboardsData]);
105
- const selectedDashboard = useMemo(() => {
106
- if (!selectedDashboardId || !dashboardsData?.data) return null;
107
- return dashboardsData.data.find(db => db._id === selectedDashboardId);
108
- }, [selectedDashboardId, dashboardsData]);
109
-
110
- const dashboardOptions = useMemo(() => {
111
- return (dashboardsData?.data || []).map(db => ({
112
- label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
113
- value: db._id
114
- }));
115
- }, [dashboardsData, t]);
116
-
117
- useEffect(() => {
118
- setSelectedModel(null)
119
- }, []);
120
-
121
-
122
- if (isLoading) {
123
- return <div>{t('loading', 'Chargement...')}</div>;
124
- }
125
-
126
- if (!selectedDashboardId && hash) {
127
- return (
128
- <div className="dashboard-not-found">
129
- <h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
130
- <p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
131
- </div>
132
- );
133
- }
134
-
135
- const creationForm = (
136
- <form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
137
- <input
138
- type="text"
139
- className="input-fit"
140
- value={newDashboardName}
141
- onChange={(e) => setNewDashboardName(e.target.value)}
142
- placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
143
- disabled={createDashboardMutation.isLoading}
144
- autoFocus
145
- />
146
- <button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
147
- {createDashboardMutation.isLoading
148
- ? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
149
- : <Trans i18nKey="btns.create">Créer</Trans>
150
- }
151
- </button>
152
- {/* Bouton pour annuler quand on a cliqué sur le "+" */}
153
- {isCreating && (
154
- <button type="button" className="btn" onClick={() => setIsCreating(false)}>
155
- <Trans i18nKey="btns.cancel">Annuler</Trans>
156
- </button>
157
- )}
158
- </form>
159
- );
160
-
161
- return (
162
- <div className="dashboards-page">
163
- <h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
164
-
165
- {isLoadingDashboards && (
166
- <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
167
- )}
168
-
169
- {errorDashboards && (
170
- <p className="error">{errorDashboards.message}</p>
171
- )}
172
-
173
- {!isLoadingDashboards && !errorDashboards && (
174
- <>
175
- {dashboardsData?.data && dashboardsData.data.length > 0 ? (
176
- <div className="dashboard-selector-container">
177
- <div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
178
- <SelectField
179
- name="dashboardSelection"
180
- label={t('dashboards.select', 'Choisir un tableau de bord :')}
181
- value={selectedDashboardId}
182
- onChange={(selectedOption) => {
183
- setSelectedDashboardId(selectedOption.value);
184
- const d = dashboardsData.data.find(f => f._id === selectedOption.value);
185
- history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
186
- }}
187
- items={dashboardOptions}
188
- />
189
- <button onClick={() => setIsCreating(true)} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
190
- <FaPlus />
191
- </button>
192
- </div>
193
- {isCreating && creationForm}
194
- </div>
195
- ) : (
196
- <>
197
- <p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
198
- {creationForm}
199
- </>
200
- )}
201
-
202
- {/* Affiche la vue du dashboard sélectionné */}
203
- {/* Passe l'objet dashboard complet */}
204
- <DashboardView dashboard={selectedDashboard} />
205
- </>
206
- )}
207
- </div>
208
- );
1
+ import React, {useState, useMemo, useEffect} from 'react';
2
+ import { Trans, useTranslation } from 'react-i18next';
3
+ import {FaPlus, FaSpinner, FaCog} from "react-icons/fa"; // Ajout FaTrash
4
+
5
+ import "./Dashboard.scss"
6
+ import {useMutation, useQuery, useQueryClient} from "react-query";
7
+ import { useAuthContext } from "./contexts/AuthContext.jsx";
8
+ import { SelectField, DurationField, TextField } from "./Field.jsx";
9
+ import {DashboardView} from "./DashboardView.jsx";
10
+ import {useModelContext} from "./contexts/ModelContext.jsx";
11
+ import {useNavigate, useParams, useSearchParams} from "react-router-dom";
12
+ import {getObjectHash} from "../../src/core.js";
13
+ import {getUserHash, getUserId} from "../../src/data.js";
14
+ import Button from "./Button.jsx";
15
+ import {FaNoteSticky} from "react-icons/fa6";
16
+
17
+ // --- DashboardsPage (Reste inchangé) ---
18
+ export function DashboardsPage() {
19
+ const { setSelectedModel, selectedModel } = useModelContext()
20
+ const { t } = useTranslation();
21
+ const { me } = useAuthContext();
22
+ const [selectedDashboardId, setSelectedDashboardId] = useState(null);
23
+
24
+ const [searchParams, setSearchParams] = useSearchParams();
25
+ const { hash } = useParams(); // 2. Récupérer le hash de l'URL
26
+
27
+ // ... après les autres hooks useState, useQuery, etc.
28
+ const queryClient = useQueryClient();
29
+ const [newDashboardName, setNewDashboardName] = useState('');
30
+ const [isCreating, setIsCreating] = useState(false);
31
+ const [isEditing, setIsEditing] = useState(false);
32
+ const [dashboardToEdit, setDashboardToEdit] = useState(null);
33
+
34
+ const createDashboardMutation = useMutation(
35
+ async (dashboardName) => {
36
+ const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
37
+ const response = await fetch('/api/data', {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify({
41
+ model: 'dashboard',
42
+ data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
43
+ })
44
+ });
45
+ if (!response.ok) {
46
+ const errorData = await response.json();
47
+ throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
48
+ }
49
+ return response.json();
50
+ },
51
+ {
52
+ onSuccess: () => {
53
+ // Rafraîchit la liste des dashboards après la création
54
+ queryClient.invalidateQueries(['userDashboards', me?.username]);
55
+ setNewDashboardName('');
56
+ setIsCreating(false);
57
+ },
58
+ onError: (error) => {
59
+ // Idéalement, afficher une notification à l'utilisateur
60
+ console.error("Erreur lors de la création du dashboard:", error);
61
+ }
62
+ }
63
+ );
64
+
65
+ const updateDashboardMutation = useMutation(
66
+ async (dashboardData) => {
67
+ const { _id, _hash, _user, _createdAt, _updatedAt, ...data } = dashboardData;
68
+ const response = await fetch(`/api/data`, {
69
+ method: 'PUT',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({
72
+ model: 'dashboard',
73
+ _id: _id,
74
+ data: data
75
+ })
76
+ });
77
+ if (!response.ok) {
78
+ const errorData = await response.json();
79
+ throw new Error(errorData.error || t('dashboards.errorUpdate', 'Impossible de mettre à jour le tableau de bord'));
80
+ }
81
+ return response.json();
82
+ },
83
+ {
84
+ onSuccess: () => {
85
+ queryClient.invalidateQueries(['userDashboards', me?.username]);
86
+ setIsEditing(false);
87
+ },
88
+ onError: (error) => {
89
+ console.error("Erreur lors de la mise à jour du dashboard:", error);
90
+ }
91
+ }
92
+ );
93
+
94
+ const handleCreateDashboard = (e) => {
95
+ e.preventDefault();
96
+ if (newDashboardName.trim()) {
97
+ createDashboardMutation.mutate(newDashboardName.trim());
98
+ }
99
+ };
100
+
101
+ const handleUpdateDashboard = (e) => {
102
+ e.preventDefault();
103
+ if (dashboardToEdit.name.trim()) {
104
+ updateDashboardMutation.mutate(dashboardToEdit);
105
+ }
106
+ };
107
+
108
+ const handleEditFieldChange = (e) => {
109
+ // e can be a standard event or an object { name, value } from custom fields
110
+ const name = e.target ? e.target.name : e.name;
111
+ const value = e.target ? e.target.value : e.value;
112
+ setDashboardToEdit(prev => ({ ...prev, [name]: value }));
113
+ };
114
+
115
+ const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
116
+ ['userDashboards', me?.username],
117
+ async () => {
118
+ if (!me?.username) return null;
119
+ const response = await fetch(
120
+ `/api/data/search?model=dashboard&_user=${me.username}`, {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ // body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
124
+ });
125
+ if (!response.ok) {
126
+ const res = await response.json();
127
+ throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
128
+ }
129
+ return await response.json();
130
+ },
131
+ {
132
+ enabled: !!me?.username
133
+ }
134
+ );
135
+ const [isLoading, setIsLoading] = useState(true);
136
+
137
+ const refreshPresets = useMemo(() => [
138
+ { label: t('dashboards.refreshPresets.none', 'Désactivé'), value: null },
139
+ { label: t('dashboards.refreshPresets.10s', '10 secondes'), value: 10 },
140
+ { label: t('dashboards.refreshPresets.30s', '30 secondes'), value: 30 },
141
+ { label: t('dashboards.refreshPresets.1m', '1 minute'), value: 60 },
142
+ { label: t('dashboards.refreshPresets.5m', '5 minutes'), value: 300 },
143
+ { label: t('dashboards.refreshPresets.15m', '15 minutes'), value: 900 },
144
+ ], [t]);
145
+ useEffect(() => {
146
+ // When a new dashboard is selected, close the editing form
147
+ setIsEditing(false);
148
+ }, [selectedDashboardId]);
149
+
150
+ useEffect(() => {
151
+ // When the edit form is opened, initialize it with the selected dashboard's data
152
+ if (isEditing && selectedDashboard) {
153
+ setDashboardToEdit({ ...selectedDashboard });
154
+ }
155
+ }, [isEditing]);
156
+
157
+ // 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
158
+ useEffect(() => {
159
+ if (dashboardsData?.data.length > 0) {
160
+ if( hash ) {
161
+ const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
162
+ setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
163
+ }else{
164
+ const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
165
+ if( !defaultDashboard) {
166
+ setSelectedDashboardId(dashboardsData.data[0]._id);
167
+ }else
168
+ setSelectedDashboardId(defaultDashboard._id || null);
169
+ }
170
+ }
171
+ setIsLoading(false);
172
+ }, [hash,dashboardsData]);
173
+ const selectedDashboard = useMemo(() => {
174
+ if (!selectedDashboardId || !dashboardsData?.data) return null;
175
+ return dashboardsData.data.find(db => db._id === selectedDashboardId);
176
+ }, [selectedDashboardId, dashboardsData]);
177
+
178
+ const dashboardOptions = useMemo(() => {
179
+ return (dashboardsData?.data || []).map(db => ({
180
+ label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
181
+ value: db._id
182
+ }));
183
+ }, [dashboardsData, t]);
184
+
185
+ const nav = useNavigate();
186
+
187
+ useEffect(() => {
188
+ setSelectedModel(null)
189
+ }, []);
190
+
191
+
192
+ if (isLoading) {
193
+ return <div>{t('loading', 'Chargement...')}</div>;
194
+ }
195
+
196
+ if (!selectedDashboardId && hash) {
197
+ return (
198
+ <div className="dashboard-not-found">
199
+ <h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
200
+ <p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ const creationForm = (
206
+ <form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
207
+ <input
208
+ type="text"
209
+ className="input-fit"
210
+ value={newDashboardName}
211
+ onChange={(e) => setNewDashboardName(e.target.value)}
212
+ placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
213
+ disabled={createDashboardMutation.isLoading}
214
+ autoFocus
215
+ />
216
+ <button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
217
+ {createDashboardMutation.isLoading
218
+ ? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
219
+ : <Trans i18nKey="btns.create">Créer</Trans>
220
+ }
221
+ </button>
222
+ {/* Bouton pour annuler quand on a cliqué sur le "+" */}
223
+ {isCreating && (
224
+ <button type="button" className="btn btn-secondary" onClick={() => setIsCreating(false)}>
225
+ <Trans i18nKey="btns.cancel">Annuler</Trans>
226
+ </button>
227
+ )}
228
+ </form>
229
+ );
230
+
231
+ return (
232
+ <div className="dashboards-page">
233
+ <div className={"flex right actions"}>
234
+ <Button onClick={() => {
235
+ nav("/user/"+getUserHash(me)+"/?model="+(selectedModel?.name||'dashboard'));
236
+ }}><FaNoteSticky /> <Trans i18nKey={"models"}>Modèles</Trans></Button>
237
+ </div>
238
+ <h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
239
+
240
+ {isLoadingDashboards && (
241
+ <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
242
+ )}
243
+
244
+ {errorDashboards && (
245
+ <p className="error">{errorDashboards.message}</p>
246
+ )}
247
+
248
+ {!isLoadingDashboards && !errorDashboards && (
249
+ <>
250
+ {dashboardsData?.data && dashboardsData.data.length > 0 ? (
251
+ <div className="dashboard-selector-container">
252
+ <div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
253
+ <SelectField
254
+ name="dashboardSelection"
255
+ label={t('dashboards.select', 'Choisir un tableau de bord :')}
256
+ value={selectedDashboardId}
257
+ onChange={(selectedOption) => {
258
+ setSelectedDashboardId(selectedOption.value);
259
+ const d = dashboardsData.data.find(f => f._id === selectedOption.value);
260
+ history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
261
+ }}
262
+ items={dashboardOptions}
263
+ />
264
+ <button onClick={() => { setIsCreating(true); setIsEditing(false); }} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
265
+ <FaPlus />
266
+ </button>
267
+ {selectedDashboard && (
268
+ <button onClick={() => { setIsEditing(true); setIsCreating(false); }} className="btn" title={t('dashboards.configure', 'Configurer le tableau de bord')}>
269
+ <FaCog />
270
+ </button>
271
+ )}
272
+ </div>
273
+ {isCreating && creationForm}
274
+ {isEditing && dashboardToEdit && (
275
+ <form onSubmit={handleUpdateDashboard} className="edit-dashboard-form flex flex-col flex-start mg-v-1 pd-1" style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
276
+ <h3 className="mg-b-1">{t('dashboards.configure', 'Configurer le tableau de bord')}</h3>
277
+ <TextField
278
+ name="name"
279
+ label={t('dashboards.dashboardName', 'Nom du tableau de bord')}
280
+ value={dashboardToEdit.name}
281
+ onChange={handleEditFieldChange}
282
+ required
283
+ autoFocus
284
+ />
285
+
286
+ <div className="flex" style={{ gap: '1rem', alignItems: 'flex-end', width: '100%' }}>
287
+ <div style={{ flexGrow: 1 }}>
288
+ <DurationField
289
+ name="refreshInterval"
290
+ label={t('dashboards.refreshInterval', 'Intervalle de rafraîchissement')}
291
+ value={dashboardToEdit.refreshInterval}
292
+ onChange={handleEditFieldChange}
293
+ help={t('dashboards.refreshIntervalHelp', 'Laisser vide pour désactiver le rafraîchissement automatique. La valeur est en secondes.')}
294
+ />
295
+ </div>
296
+ <SelectField
297
+ name="refreshIntervalPreset"
298
+ label={t('dashboards.refreshPresets.label', 'Préréglages')}
299
+ value={dashboardToEdit.refreshInterval}
300
+ onChange={(option) => {
301
+ console.log({option});
302
+ if (option?.value)
303
+ handleEditFieldChange({ name: 'refreshInterval', value: option?.value })
304
+ }}
305
+ items={refreshPresets}
306
+ style={{ minWidth: '150px' }}
307
+ />
308
+ </div>
309
+ <div className="flex mg-t-1" style={{ gap: '8px' }}>
310
+ <button type="submit" className="btn" disabled={updateDashboardMutation.isLoading}>
311
+ {updateDashboardMutation.isLoading ? <><FaSpinner className="spin" /> <Trans i18nKey="btns.saving">Enregistrement...</Trans></> : <Trans i18nKey="btns.save">Enregistrer</Trans>}
312
+ </button>
313
+ <button type="button" className="btn btn-secondary" onClick={() => setIsEditing(false)}>
314
+ <Trans i18nKey="btns.cancel">Annuler</Trans>
315
+ </button>
316
+ </div>
317
+ </form>
318
+ )}
319
+ </div>
320
+ ) : (
321
+ <>
322
+ <p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
323
+ {creationForm}
324
+ </>
325
+ )}
326
+
327
+ {/* Affiche la vue du dashboard sélectionné */}
328
+ {/* Passe l'objet dashboard complet */}
329
+ {/*
330
+ TODO in DashboardView.jsx:
331
+ Utiliser la nouvelle propriété `dashboard.refreshInterval` (en secondes).
332
+ Si `refreshInterval` est défini et supérieur à 0, les données du dashboard (KPIs, graphiques)
333
+ doivent être rafraîchies automatiquement à cet intervalle.
334
+ Exemple avec react-query: `useQuery(queryKey, queryFn, { refetchInterval: dashboard.refreshInterval * 1000 })`
335
+ ou avec useEffect/setInterval:
336
+ useEffect(() => {
337
+ if (dashboard?.refreshInterval > 0) {
338
+ const intervalId = setInterval(() => {
339
+ // logique de rafraîchissement, ex: queryClient.invalidateQueries(...)
340
+ }, dashboard.refreshInterval * 1000);
341
+ return () => clearInterval(intervalId);
342
+ }
343
+ }, [dashboard?.refreshInterval, queryClient]);
344
+ */}
345
+ <DashboardView dashboard={selectedDashboard} />
346
+ </>
347
+ )}
348
+ </div>
349
+ );
209
350
  }