data-primals-engine 1.0.10 → 1.1.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.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +5390 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. package/vitest.config.js +2 -2
@@ -0,0 +1,709 @@
1
+ import React, {forwardRef, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
2
+
3
+ import "./App.scss";
4
+ import {useMutation, useQuery, useQueryClient} from "react-query";
5
+ import {CheckboxField, FileField, NumberField, SelectField, TextField} from "./Field.jsx";
6
+ import ModelCreator from "./ModelCreator.jsx";
7
+ import {useModelContext} from "./contexts/ModelContext.jsx";
8
+ import {FaMagnifyingGlass, FaPencil} from "react-icons/fa6";
9
+ import {Dialog, DialogProvider} from "./Dialog.jsx";
10
+ import {Pagination} from "./Pagination.jsx";
11
+ import {
12
+ elementsPerPage, kilobytes,
13
+ mainFieldsTypes,
14
+ maxBytesPerSecondThrottleData,
15
+ maxFileSize,
16
+ maxRequestData, metaModels
17
+ } from "data-primals-engine/constants";
18
+ import {
19
+ FaArrowDown,
20
+ FaArrowUp, FaBell,
21
+ FaBook, FaCopy,
22
+ FaEdit,
23
+ FaFileExport, FaFileImport,
24
+ FaFilter, FaInfo,
25
+ FaLanguage,
26
+ FaLock,
27
+ FaPlus,
28
+ FaTrash
29
+ } from "react-icons/fa";
30
+ import {getDefaultForType, getUserId} from "data-primals-engine/data";
31
+ import {Trans, useTranslation} from "react-i18next";
32
+
33
+ import {debounce, escapeRegExp, event_trigger, getObjectHash, isGUID} from "data-primals-engine/core";
34
+ import Button from "./Button.jsx";
35
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
36
+ import {RTE} from "./RTE.jsx";
37
+ import APIInfo from "./APIInfo.jsx";
38
+ import {xt256} from "react-code-blocks";
39
+ import {isList} from "@tiptap/react";
40
+ import {useNotificationContext} from "./NotificationProvider.jsx";
41
+ import modelCreator from "./ModelCreator.jsx";
42
+ import RelationField from "./RelationField.jsx";
43
+ import RelationValue from "./RelationValue.jsx";
44
+ import {ModelImporter} from "./ModelImporter.jsx";
45
+ import {DataTable} from "./DataTable.jsx";
46
+ import {ModelList} from "./ModelList.jsx";
47
+ import {DataEditor} from "./DataEditor.jsx";
48
+ import TourSpotlight from "./TourSpotlight.jsx";
49
+ import useLocalStorage from "./hooks/useLocalStorage.js";
50
+ import {useUI} from "./contexts/UIContext.jsx";
51
+ import {useTutorials} from "./hooks/useTutorials.jsx";
52
+ import ViewSwitcher from "./ViewSwitcher.jsx";
53
+ import KanbanConfigModal from "./KanbanConfigModal.jsx";
54
+ import CalendarConfigModal from "./CalendarConfigModal.jsx";
55
+ import KanbanView from "./KanbanView.jsx";
56
+
57
+
58
+ // --- AJOUT : Composants Placeholders pour la démonstration ---
59
+ const CalendarView = ({ settings, model }) => (
60
+ <div className="p-4 border rounded-md mt-4 bg-gray-50">
61
+ <h3 className="font-bold">Vue Calendrier</h3>
62
+ <p>Affichage des données pour le modèle <strong>{model?.name}</strong> en utilisant le champ de date : <strong>{settings.dateField}</strong>.</p>
63
+ {/* L'implémentation réelle du calendrier irait ici */}
64
+ </div>
65
+ );
66
+
67
+ const NotConfiguredPlaceholder = ({ type, onConfigure }) => (
68
+ <div className="p-4 border border-dashed rounded-md mt-4 text-center bg-gray-50">
69
+ <h4><Trans i18nKey="dataview.notConfiguredTitle" values={{ type }}>Vue {{type}} non configurée</Trans></h4>
70
+ <p className="text-sm text-gray-600"><Trans i18nKey="dataview.notConfiguredText">Veuillez configurer cette vue pour l'utiliser.</Trans></p>
71
+ <Button onClick={onConfigure} className="mt-2">
72
+ <Trans i18nKey="dataview.configureButton" values={{ type }}>Configurer {{type}}</Trans>
73
+ </Button>
74
+ </div>
75
+ );
76
+
77
+ function DataLayout() {
78
+ const [currentView, setCurrentView] = useState('table');
79
+
80
+ // Nouvelle version (persistante)
81
+ const [viewSettings, setViewSettings] = useLocalStorage('viewSettings', {});
82
+
83
+ const [isCalendarModalOpen, setCalendarModalOpen] = useState(false);
84
+ const [isKanbanModalOpen, setKanbanModalOpen] = useState(false);
85
+
86
+ const { triggerTutorialCheck } = useTutorials();
87
+ const { t, i18n } = useTranslation();
88
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
89
+
90
+ const [filterValues, setFilterValues] = useState({});
91
+ const { dataByModel,paginatedDataByModel,
92
+ setRelationFilters, setSelectedModel, selectedModel,
93
+ setFilteredDatasToLoad,
94
+ setRelationIds,
95
+ setOnSuccessCallbacks,
96
+ datasToLoad,
97
+ setDatasToLoad,page, setPage, countByModel, relationIds,
98
+ pagedFilters, pagedSort,
99
+ elementsPerPage,
100
+ setRelations,
101
+ generatedModels
102
+ } = useModelContext(); // Utilisez le contexte
103
+ const queryClient = useQueryClient();
104
+
105
+ const isDataLoaded = true;
106
+ const [checkedItems, setCheckedItems] = useState([])
107
+
108
+ const [refreshTime, setRefreshTime] = useState(0);
109
+ const [formData, setFormData] = useState({});
110
+ const [recordToEdit, setRecordToEdit] = useState(null); // New state for record to edit
111
+
112
+ const mainPartRef = useRef();
113
+ const modelCreatorRef = useRef();
114
+
115
+ const [importModalVisible, setImportModalVisible] = useState(false);
116
+ const [editionMode, setEditionMode] = useState(false);
117
+ const [showDataEditor, setDataEditorVisible] = useState(false);
118
+ const [showAPIInfo, setAPIInfoVisible] = useState(false);
119
+
120
+
121
+ // --- MODIFICATION : Logique de changement de vue mise à jour ---
122
+ const handleSwitchView = (viewName) => {
123
+ console.log({viewName})
124
+ if (viewName === 'table') {
125
+ setCurrentView('table');
126
+ return;
127
+ }
128
+
129
+ if (!selectedModel) {
130
+ addNotification({ title: t('datalayout.selectModelFirst', 'Veuillez d\'abord sélectionner un modèle.'), status: 'warning' });
131
+ return;
132
+ }
133
+
134
+ const modelSettings = viewSettings[selectedModel.name] || {};
135
+
136
+ if (viewName === 'calendar') {
137
+ if (modelSettings.calendar?.dateField) {
138
+ setCurrentView('calendar');
139
+ } else {
140
+ setCalendarModalOpen(true);
141
+ }
142
+ } else if (viewName === 'kanban') {
143
+ if (modelSettings.kanban?.groupByField) {
144
+ setCurrentView('kanban');
145
+ } else {
146
+ setKanbanModalOpen(true);
147
+ }
148
+ }
149
+ };
150
+
151
+ // --- MODIFICATION : Sauvegarde dans localStorage ---
152
+ const handleSaveCalendarConfig = (config) => {
153
+ if (!selectedModel) return;
154
+ setViewSettings(prev => ({
155
+ ...prev,
156
+ [selectedModel.name]: {
157
+ ...(prev[selectedModel.name] || {}),
158
+ calendar: config,
159
+ },
160
+ }));
161
+ setCalendarModalOpen(false);
162
+ setCurrentView('calendar');
163
+ };
164
+
165
+ const handleSaveKanbanConfig = (config) => {
166
+ if (!selectedModel) return;
167
+ setViewSettings(prev => ({
168
+ ...prev,
169
+ [selectedModel.name]: {
170
+ ...(prev[selectedModel.name] || {}),
171
+ kanban: config,
172
+ },
173
+ }));
174
+ setKanbanModalOpen(false);
175
+ setCurrentView('kanban');
176
+ };
177
+
178
+ // --- MODIFICATION : Dérive les settings du modèle courant depuis l'at global ---
179
+ const currentModelViewSettings = useMemo(() => {
180
+ if (!selectedModel) return {};
181
+ return viewSettings[selectedModel.name] || {};
182
+ }, [viewSettings, selectedModel]);
183
+
184
+ // --- MODIFICATION : Vérifie si les vues sont configurées pour le modèle courant ---
185
+ const configuredViews = useMemo(() => {
186
+ if (!selectedModel) return { calendar: false, kanban: false };
187
+ const modelSettings = viewSettings[selectedModel.name] || {};
188
+ return {
189
+ calendar: !!modelSettings.calendar?.dateField,
190
+ kanban: !!modelSettings.kanban?.groupByField,
191
+ };
192
+ }, [viewSettings, selectedModel]);
193
+
194
+ // --- AJOUT : Logique de rendu de la vue courante ---
195
+ const renderCurrentView = () => {
196
+ if (!selectedModel) return null;
197
+
198
+ switch (currentView) {
199
+ case 'calendar':
200
+ return configuredViews.calendar
201
+ ? <CalendarView settings={currentModelViewSettings.calendar} model={selectedModel} />
202
+ : <NotConfiguredPlaceholder type="calendar" onConfigure={() => setCalendarModalOpen(true)} />;
203
+ case 'kanban':
204
+ return configuredViews.kanban
205
+ ? <KanbanView settings={currentModelViewSettings.kanban} model={selectedModel} />
206
+ : <NotConfiguredPlaceholder type="kanban" onConfigure={() => setKanbanModalOpen(true)} />;
207
+ case 'table':
208
+ default:
209
+ // Le DataTable existant est retourné par défaut
210
+ return <DataTable
211
+ checkedItems={checkedItems}
212
+ setCheckedItems={setCheckedItems}
213
+ filterValues={filterValues}
214
+ setFilterValues={setFilterValues}
215
+ model={selectedModel}
216
+ onAddData={(model) => {
217
+ mainPartRef.current.scrollIntoView({behavior: "smooth"});
218
+ handleAddData(model);
219
+ }}
220
+ onShowAPI={() => {
221
+ setAPIInfoVisible(true);
222
+ setDataEditorVisible(false);
223
+ setEditionMode(false);
224
+ }}
225
+ onEdit={(item) => {
226
+ mainPartRef.current.scrollIntoView({behavior: "smooth"});
227
+ setRecordToEdit(item);
228
+ setFormData(item);
229
+ setDataEditorVisible(true);
230
+ }}
231
+ onDelete={(item) => {
232
+ queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, elementsPerPage, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
233
+ }}
234
+ />;
235
+ }
236
+ };
237
+
238
+ const handleModelSelect = (model) => {
239
+ setRelationFilters({});
240
+ setCurrentView('table');
241
+ setCheckedItems([])
242
+ setFilterValues({});
243
+ if (!model) {
244
+ return;
245
+ }
246
+
247
+ const dt = [];
248
+ const t = [...model.fields].reduce((acc, field, index) => {
249
+ if (field.type === "relation") {
250
+ dt.push(field.relation);
251
+ acc[field.name] = dataByModel[field.relation]?.length > 0 ? dataByModel[field.relation][0]._id : null;
252
+ } else {
253
+ acc[field.name] = getDefaultForType(field);
254
+ }
255
+ return acc;
256
+ }, {});
257
+ setPage(1);
258
+ setFormData(t);
259
+ setFilteredDatasToLoad([model.name]);
260
+
261
+ setRecordToEdit(null); // Clear record to edit when model changes
262
+
263
+ let tl = [];
264
+ model.fields.forEach(field => {
265
+ if (field.type === 'relation') {
266
+ if (!tl.includes(field.relation))
267
+ tl.push(field.relation);
268
+ }
269
+ });
270
+ setDatasToLoad(tl);
271
+
272
+ const cb = ({data}) => {
273
+
274
+ updateRelationIds(model, data);
275
+
276
+ if( datasToLoad.length === 0 ) {
277
+ model.fields.forEach(field => {
278
+ if (field.type === 'relation') {
279
+ if (!tl.includes(field.relation))
280
+ tl.push(field.relation);
281
+ }
282
+ });
283
+ setDatasToLoad(tl);
284
+ }
285
+ };
286
+ // add new data
287
+ setOnSuccessCallbacks(cbs => {
288
+ const c = {...cbs};
289
+ if( !c['api/data/' + model.name + '/paged'])
290
+ c['api/data/' + model.name + '/paged'] = {};
291
+ c['api/data/' + model.name + '/paged'][model.name] = cb;
292
+ return c;
293
+ })
294
+ setSelectedModel(model);
295
+
296
+ gtag("event", "select_content", {
297
+ content_type: "model",
298
+ content_id: model.name
299
+ });
300
+ //queryClient.invalidateQueries(['api/data', model.name, 'page', page]);
301
+ }
302
+
303
+ const { me } = useAuthContext();
304
+
305
+ const { addNotification } = useNotificationContext();
306
+
307
+ const { mutate: insertOrUpdateMutation, isLoading} = useMutation(({formData,record}) => {
308
+ const method = record ? 'PUT' : 'POST'; // Determine method based on record
309
+ const url = record ? `/api/data/${record._id}` : `/api/data`; // Determine URL
310
+
311
+ try {
312
+ const fd = new FormData();
313
+
314
+ let obj = {};
315
+ for (const key in formData) {
316
+ if (formData[key] !== undefined)
317
+ obj[key] = formData[key];
318
+ }
319
+ fd.append("_data", JSON.stringify({...obj, _hash: undefined, _id: undefined}));
320
+
321
+ Array.from(document.querySelectorAll('.field-file input[data-field]')).forEach(input =>{
322
+ const fieldName = input.dataset['field'];
323
+ if (input.files.length > 0) {
324
+ Array.from(input.files).forEach((file, index) => {
325
+ if( file)
326
+ fd.append(`${fieldName}[${index}]`, file);
327
+ });
328
+ }
329
+ });
330
+ fd.append('model', selectedModel.name);
331
+
332
+ ///fd.append("files", fd2);
333
+ return fetch(`${url}?lang=${lang}&_user=${encodeURIComponent(getUserId(me))}`, {
334
+ method,
335
+ body: fd
336
+ }).then(e => e.json());
337
+
338
+ } catch (error) {
339
+ console.error('Erreur lors de l\'enregistrement des données:', error);
340
+ // Handle error, e.g., display error message to the user
341
+ }
342
+ }, {
343
+ onError: (err)=>{
344
+ const notificationData = {
345
+ title: 'Erreur lors de l\'enregistrement des données',
346
+ status: 'error'
347
+ };
348
+ addNotification(notificationData);
349
+ },
350
+ onSuccess: (data) => {
351
+
352
+ console.log('Données enregistrées:', data, selectedModel);
353
+
354
+ event_trigger(recordToEdit ? 'API_ADD_DATA' : 'API_ADD_DATA', {
355
+ model: selectedModel.name,
356
+ });
357
+
358
+ gtag("event", "select_content", {
359
+ content_type: "edit_data",
360
+ content_id: selectedModel.name
361
+ });
362
+
363
+ const notificationData = {
364
+ title: data.success ? t('dataimporter.success', 'Données enregistrées') : t(data.error, data.error),
365
+ icon: data.success ? <FaInfo /> : undefined,
366
+ status: data.success ? 'completed': 'error'
367
+ };
368
+ addNotification(notificationData);
369
+
370
+ updateRelationIds(selectedModel, formData);
371
+ setDatasToLoad([...datasToLoad, selectedModel.name]);
372
+ queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, elementsPerPage, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
373
+
374
+ if(data.inserted) {
375
+ const t = [...selectedModel.fields].reduce((acc, field, index) => {
376
+ if (field.type === "relation") {
377
+ acc[field.name] = field.multiple ? [] : null;
378
+ } else {
379
+ acc[field.name] = getDefaultForType(field);
380
+ }
381
+ return acc;
382
+ }, {});
383
+ setFormData(t)
384
+ }
385
+
386
+ setRelations({});
387
+
388
+ }})
389
+ const handleFormSubmit = async (formData, record) => { // Add record parameter
390
+ insertOrUpdateMutation({formData, record})
391
+ };
392
+
393
+ const updateRelationIds = (model, data) => {
394
+ const r = {...relationIds};
395
+ let datas = !Array.isArray(data) ? [data] : data;
396
+ model.fields.forEach(field => {
397
+ if( field.type !== "relation")
398
+ return;
399
+ datas.forEach(d => {
400
+ const value = d[field.name];
401
+ if (field.multiple && Array.isArray(value)) {
402
+ for (let i = 0; i < value.length; i++) {
403
+ if (!r[field.relation]){
404
+ r[field.relation] = [];
405
+ }
406
+ if (!r[field.relation].includes(value[i])) {
407
+ r[field.relation].push(value[i]);
408
+ }
409
+ }
410
+ } else if (!field.multiple && value) {
411
+ if (!r[field.relation]){
412
+ r[field.relation] = [];
413
+ }
414
+ if (!r[field.relation].includes(value)) {
415
+ r[field.relation].push(value);
416
+ }
417
+ }
418
+ });
419
+ });
420
+ setRelationIds(r);
421
+ queryClient.invalidateQueries(['api/data', model.name, r]);
422
+ }
423
+
424
+ useEffect(() => {
425
+ setSelectedModel(null)
426
+ setDataEditorVisible(false);
427
+ setAPIInfoVisible(false);
428
+ setEditionMode(true);
429
+ }, []);
430
+
431
+ const deleteMutation = useMutation((selectedModels) => {
432
+ return fetch('/api/data/'+checkedItems.map(m => m._id).join(',')+'?lang='+lang+'&_user='+encodeURIComponent(getUserId(me)), {
433
+ method: 'DELETE', headers: {
434
+ 'Content-Type': 'application/json'
435
+ }
436
+ }).then(e => e.json());
437
+ }, { onSuccess: (data) => {
438
+ if( data.success ){
439
+ queryClient.invalidateQueries(['api/data', selectedModel?.name, 'page', page, elementsPerPage, pagedFilters[selectedModel?.name], pagedSort[selectedModel?.name]]);
440
+ }
441
+
442
+ const notificationData = {
443
+ id: 'dataimporter.success',
444
+ title: t('dataimporter.success', 'Données supprimées'),
445
+ icon: <FaInfo />,
446
+ status: 'completed'
447
+ };
448
+ addNotification(notificationData);
449
+ }, onError:(err)=>{
450
+ const notificationData = {
451
+ id: 'dataimporter.error',
452
+ title: err.message,
453
+ status: 'error'
454
+ };
455
+ addNotification(notificationData);
456
+ }});
457
+ const handleDeletion = () => {
458
+ deleteMutation.mutate();
459
+ setCheckedItems([]);
460
+ }
461
+ const importModelsMutation = useMutation((selectedModels) => {
462
+ return fetch('/api/models/import', { method: 'POST', headers: {
463
+ 'Content-Type': 'application/json'
464
+ },
465
+ body: JSON.stringify({ models: selectedModels.map(m => m.name) })
466
+ })
467
+ });
468
+
469
+ const handleConfigureCurrentView = () => {
470
+ if (!selectedModel) return;
471
+
472
+ switch (currentView) {
473
+ case 'kanban':
474
+ setKanbanModalOpen(true);
475
+ break;
476
+ case 'calendar':
477
+ setCalendarModalOpen(true);
478
+ break;
479
+ default:
480
+ // Pas de configuration pour la vue 'table'
481
+ break;
482
+ }
483
+ };
484
+
485
+ const handleAddData = (model)=>{
486
+
487
+ handleModelSelect(model);
488
+ const t = model ? [...model.fields].reduce((acc, field) => {
489
+ if( field.type === "relation"){
490
+ acc[field.name] = field.multiple ? [] : null;
491
+ }else {
492
+ acc[field.name] = getDefaultForType(field);
493
+ }
494
+ return acc;
495
+ }, {}) : [];
496
+ setFormData(t)
497
+ setEditionMode(false);
498
+ setDataEditorVisible(true);
499
+ setAPIInfoVisible(false);
500
+ gtag("event", "select_content", {
501
+ content_type: "select_model",
502
+ content_id: model.name
503
+ });
504
+ }
505
+
506
+ const onImportModels = (models) => {
507
+ importModelsMutation.mutateAsync(models).then(e => {
508
+ queryClient.invalidateQueries('api/models');
509
+ });
510
+ setImportModalVisible(false);
511
+ }
512
+
513
+ useEffect(() => {
514
+ if( showDataEditor && mainPartRef.current ){
515
+
516
+ }
517
+ }, [showDataEditor,mainPartRef.current]);
518
+
519
+
520
+ const [currentProfile, setCurrentProfile] = useLocalStorage('profile', null);
521
+ const {isTourOpen, setIsTourOpen, currentTourSteps, allTourSteps, setTourStepIndex, setCurrentTourSteps, currentTour,setCurrentTour} = useUI();
522
+
523
+ const startTour = () => {
524
+ setIsTourOpen(true);
525
+ };
526
+
527
+ const closeTour = (completedTourName) => {
528
+ // On génère le nom du tour de démo pour le comparer
529
+ const demoTourName = `tour_${getObjectHash({steps: allTourSteps.demo || []})}`;
530
+
531
+ // Si le tour qui vient de se terminer est le tour de démo
532
+ if (completedTourName === demoTourName) {
533
+
534
+ setIsTourOpen(false);
535
+ }
536
+ };
537
+
538
+ useEffect(() => {
539
+ if( !currentProfile ){
540
+ setIsTourOpen(true);
541
+ }
542
+ }, [currentProfile])
543
+
544
+ useEffect(() => {
545
+ // Si un modèle est sélectionné...
546
+ if (selectedModel) {
547
+ // Récupérer les étapes du tour "datapacks"
548
+ const datapacksSteps = allTourSteps.datapacks;
549
+ // S'assurer que ce tour existe et a des étapes
550
+ if (datapacksSteps && datapacksSteps.length > 0) {
551
+ // 1. Définir les étapes du tour qui doit être affiché
552
+ setCurrentTourSteps(datapacksSteps);
553
+ // 2. Calculer le nom unique du tour et le définir comme tour courant
554
+ // C'est l'clé qui manquait.
555
+ const tourName = `tour_${getObjectHash({ steps: datapacksSteps })}`;
556
+ setCurrentTour(tourName);
557
+ // 3. Activer l'affichage du composant de tour
558
+ setIsTourOpen(true);
559
+ }
560
+ }
561
+ }, [selectedModel, allTourSteps]); // Ajout de allTourSteps aux dépendances pour la bonne pratique
562
+
563
+ return (
564
+ <>
565
+ <>{/^demo[0-9]{1,2}$/.test(me.username) && (
566
+ <TourSpotlight
567
+ name={"tour_"+getObjectHash({steps:currentTourSteps})}
568
+ steps={currentTourSteps}
569
+ isOpen={isTourOpen}
570
+ onClose={closeTour}
571
+ />)}</>
572
+ <div className="datalayout flex flex-start">
573
+ <ModelList tourSteps={currentTourSteps}
574
+ onAPIInfo={(model) => {
575
+ setAPIInfoVisible(true);
576
+ setDataEditorVisible(false);
577
+ setEditionMode(false);
578
+ setSelectedModel(model);
579
+ gtag("event", "select_content", {
580
+ content_type: "api",
581
+ content_id: model.name
582
+ });
583
+ }} onImportModel={() => {
584
+ setImportModalVisible(true);
585
+ }} onCreateModel={() => {
586
+ setSelectedModel(null);
587
+ setAPIInfoVisible(false);
588
+ setDataEditorVisible(false);
589
+ setEditionMode(true);
590
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
591
+ gtag("event", "select_content", {
592
+ content_type: "create_model",
593
+ });
594
+ }} onEditModel={(model) => {
595
+ handleModelSelect(model);
596
+ setDataEditorVisible(false);
597
+ setAPIInfoVisible(false);
598
+ setEditionMode(true);
599
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
600
+ gtag("event", "select_content", {
601
+ content_type: "edit_model",
602
+ content_id: model.name
603
+ });
604
+ }} onModelSelect={(model) => {
605
+ handleModelSelect(model);
606
+ setDataEditorVisible(false);
607
+ setEditionMode(false);
608
+ setAPIInfoVisible(false);
609
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
610
+ gtag("event", "select_content", {
611
+ content_type: "select_model",
612
+ content_id: model.name
613
+ });
614
+ }} onNewData={(model) => {
615
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
616
+ handleAddData(model);
617
+ }}/>
618
+ {(editionMode) && (
619
+ <ModelCreator ref={modelCreatorRef} onModelGenerated={() =>{
620
+ modelCreatorRef.current.scrollIntoView({behavior: 'smooth'});
621
+ }} initialModel={selectedModel} onModelSaved={(model) => {
622
+ setRefreshTime(new Date().getTime());
623
+ handleModelSelect(model);
624
+ }}/>)}
625
+
626
+ <div className="hidden-anchor" ref={mainPartRef}></div>
627
+
628
+ {showDataEditor && (<DataEditor
629
+ key={selectedModel?.name}
630
+ isLoading={isLoading}
631
+ model={selectedModel}
632
+ formData={formData}
633
+ setFormData={setFormData}
634
+ refreshTime={refreshTime}
635
+ onSubmit={handleFormSubmit}
636
+ setRecord={setRecordToEdit}
637
+ record={recordToEdit} // Pass record to edit to DataEditor
638
+ />)}
639
+
640
+
641
+ {selectedModel && showAPIInfo && <APIInfo/>}
642
+ {selectedModel && !showAPIInfo && !generatedModels.some(g => g.name === selectedModel?.name) && (<div className="datas">
643
+
644
+ {<ViewSwitcher
645
+ currentView={currentView}
646
+ onViewChange={handleSwitchView}
647
+ configuredViews={configuredViews}
648
+ onConfigureView={handleConfigureCurrentView}
649
+ />}
650
+ <h2>{t(`model_${selectedModel?.name}`, selectedModel?.name)} <>({countByModel?.[selectedModel?.name]})</></h2>
651
+ {t(`model_${selectedModel?.name}`, selectedModel?.name) !== selectedModel?.name && (
652
+ <span className="badge"><strong>model</strong> : {selectedModel?.name}</span>)}
653
+ <p className="model-desc hint">{t(`model_description_${selectedModel.name}`, selectedModel.description)}</p>
654
+
655
+ {renderCurrentView()}
656
+
657
+ {isDataLoaded && currentView === 'table' && (<>
658
+ {selectedModel && (<Pagination showElementsPerPage={true} onChange={page => {
659
+ setPage(page);
660
+ setCheckedItems([]);
661
+ gtag("event", "select_content", {
662
+ content_type: "change_page",
663
+ content_id: page
664
+ });
665
+ queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
666
+ }} page={page} setPage={setPage} totalCount={countByModel[selectedModel.name]}
667
+ hasPreviousNext={true} visibleItemsCount={5}
668
+ elementsPerPage={elementsPerPage}/>)}
669
+ <div className="actions flex">
670
+ <Button onClick={() => {
671
+ setCheckedItems(paginatedDataByModel[selectedModel.name]);
672
+ }}><Trans i18nKey={"datatable.selectAll"}>Tout sélectionner</Trans></Button>
673
+ <Button onClick={handleDeletion} disabled={!checkedItems?.length}><Trans
674
+ i18nKey={"datatable.deleteSelection"}>Supprimer la sélection</Trans></Button>
675
+ </div>
676
+ </>)}
677
+ </div>)}
678
+ </div>
679
+
680
+ <DialogProvider>
681
+ {importModalVisible && (<Dialog onClose={() => {
682
+ setImportModalVisible(false)
683
+ }} isModal={true} isClosable={true}>
684
+ <ModelImporter onImport={onImportModels}/>
685
+ </Dialog>)}
686
+
687
+ <CalendarConfigModal
688
+ isOpen={isCalendarModalOpen}
689
+ onClose={() => setCalendarModalOpen(false)}
690
+ onSave={handleSaveCalendarConfig}
691
+ modelFields={selectedModel?.fields ||[]}
692
+ initialSettings={currentModelViewSettings.calendar}
693
+ />
694
+
695
+ <KanbanConfigModal
696
+ isOpen={isKanbanModalOpen}
697
+ onClose={() => setKanbanModalOpen(false)}
698
+ onSave={handleSaveKanbanConfig}
699
+ model={selectedModel}
700
+ modelFields={selectedModel?.fields||[]}
701
+ initialSettings={currentModelViewSettings.kanban}
702
+ />
703
+ </DialogProvider>
704
+ </>
705
+ );
706
+ }
707
+
708
+
709
+ export default DataLayout;