data-primals-engine 1.5.0 → 1.5.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 (52) hide show
  1. package/README.md +35 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +12 -0
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +805 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +47 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +92 -53
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +262 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +2 -1
  38. package/src/constants.js +1 -1
  39. package/src/defaultModels.js +1596 -1544
  40. package/src/i18n.js +710 -10
  41. package/src/modules/assistant/assistant.js +148 -18
  42. package/src/modules/bucket.js +2 -1
  43. package/src/modules/data/data.core.js +118 -92
  44. package/src/modules/data/data.history.js +531 -492
  45. package/src/modules/data/data.js +3 -53
  46. package/src/modules/data/data.operations.js +77 -26
  47. package/src/modules/data/data.relations.js +686 -686
  48. package/src/modules/data/data.routes.js +1879 -1821
  49. package/src/modules/data/data.validation.js +81 -2
  50. package/src/modules/file.js +247 -238
  51. package/src/packs.js +5482 -5478
  52. package/test/data.integration.test.js +1115 -1060
@@ -1,756 +1,806 @@
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 ModelCreator from "./ModelCreator.jsx";
6
- import {useModelContext} from "./contexts/ModelContext.jsx";
7
- import {Dialog, DialogProvider} from "./Dialog.jsx";
8
- import {Pagination} from "./Pagination.jsx";
9
- import {Event} from "../../src/events.js";
10
-
11
- import {
12
- FaEye,
13
- FaFilter, FaInfo,
14
- } from "react-icons/fa";
15
- import {getDefaultForType, getUserHash, getUserId} from "../../src/data.js";
16
- import {Trans, useTranslation} from "react-i18next";
17
-
18
- import {getObjectHash} from "../../src/core.js";
19
- import Button from "./Button.jsx";
20
- import {useAuthContext} from "./contexts/AuthContext.jsx";
21
- import APIInfo from "./APIInfo.jsx";
22
- import {useNotificationContext} from "./NotificationProvider.jsx";
23
- import {ModelImporter} from "./ModelImporter.jsx";
24
- import {DataTable} from "./DataTable.jsx";
25
- import {ModelList} from "./ModelList.jsx";
26
- import {DataEditor} from "./DataEditor.jsx";
27
- import TourSpotlight from "./TourSpotlight.jsx";
28
- import useLocalStorage from "./hooks/useLocalStorage.js";
29
- import {useUI} from "./contexts/UIContext.jsx";
30
- import {useTutorials} from "./hooks/useTutorials.jsx";
31
- import ViewSwitcher from "./ViewSwitcher.jsx";
32
- import KanbanConfigModal from "./KanbanConfigModal.jsx";
33
- import CalendarConfigModal from "./CalendarConfigModal.jsx";
34
- import KanbanView from "./KanbanView.jsx";
35
- import CalendarView from "./CalendarView.jsx";
36
- import {useLocation, useParams, useSearchParams} from "react-router-dom";
37
- import { useNavigate } from "react-router-dom";
38
-
39
-
40
- const NotConfiguredPlaceholder = ({ type, onConfigure }) => (
41
- <div className="p-4 border border-dashed rounded-md mt-4 text-center bg-gray-50">
42
- <h4><Trans i18nKey="dataview.notConfiguredTitle" values={{ type }}>Vue {{type}} non configurée</Trans></h4>
43
- <p className="text-sm text-gray-600"><Trans i18nKey="dataview.notConfiguredText">Veuillez configurer cette vue pour l'utiliser.</Trans></p>
44
- <Button onClick={onConfigure} className="mt-2">
45
- <Trans i18nKey="dataview.configureButton" values={{ type }}>Configurer {{type}}</Trans>
46
- </Button>
47
- </div>
48
- );
49
-
50
- function DataLayout({refreshUI}) {
51
- const [ searchParams, setSearchParams ] = useSearchParams();
52
- const [viewSettings, setViewSettings] = useLocalStorage('viewSettings', {});
53
-
54
- const [isCalendarModalOpen, setCalendarModalOpen] = useState(false);
55
- const [isKanbanModalOpen, setKanbanModalOpen] = useState(false);
56
-
57
- const { triggerTutorialCheck } = useTutorials();
58
- const { t, i18n } = useTranslation();
59
- const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
60
-
61
- // Stocke la vue sélectionnée pour chaque modèle. Ex: { "contacts": "calendar", "tasks": "kanban" }
62
- const [viewsByModel, setViewsByModel] = useLocalStorage('dataLayout_viewsByModel', {});
63
-
64
- const [filterValues, setFilterValues] = useState({});
65
- const { dataByModel,paginatedDataByModel,
66
- setRelationFilters, setSelectedModel, selectedModel,
67
- setFilteredDatasToLoad,
68
- setRelationIds,
69
- setOnSuccessCallbacks,
70
- datasToLoad,
71
- setDatasToLoad,page, setPage, countByModel, relationIds,
72
- pagedFilters, pagedSort,
73
- elementsPerPage,
74
- setRelations,
75
- generatedModels,
76
- models
77
- } = useModelContext(); // Utilisez le contexte
78
- const queryClient = useQueryClient();
79
-
80
- const isDataLoaded = true;
81
- const [checkedItems, setCheckedItems] = useState([])
82
-
83
- const [refreshTime, setRefreshTime] = useState(0);
84
- const [formData, setFormData] = useState({});
85
- const [recordToEdit, setRecordToEdit] = useState(null); // New state for record to edit
86
-
87
- const mainPartRef = useRef();
88
- const modelCreatorRef = useRef();
89
-
90
- const [importModalVisible, setImportModalVisible] = useState(false);
91
- const [editionMode, setEditionMode] = useState(false);
92
- const [showDataEditor, setDataEditorVisible] = useState(false);
93
- const [showAPIInfo, setAPIInfoVisible] = useState(false);
94
- const nav = useNavigate();
95
- const mod = searchParams.get('model');
96
- const loc = useLocation();
97
- useEffect(() =>{
98
- if (selectedModel?.name) {
99
- nav('/user/' + getUserHash(me) + '/?model=' + selectedModel?.name);
100
- }
101
- }, [selectedModel?.name])
102
-
103
-
104
- useEffect(() => {
105
- setSelectedModel(null)
106
- setDataEditorVisible(false);
107
- setAPIInfoVisible(false);
108
- setEditionMode(true);
109
- }, []);
110
-
111
- useEffect(() =>{
112
- const m = models.find(f => f.name === mod);
113
- setSelectedModel(m);
114
- setEditionMode(!m);
115
- }, [mod, models, searchParams])
116
-
117
-
118
- // La vue courante est dérivée du modèle sélectionné et des préférences stockées.
119
- const currentView = useMemo(() => {
120
- if (!selectedModel) return 'table';
121
- return viewsByModel[selectedModel.name] || 'table';
122
- }, [selectedModel, viewsByModel]);
123
-
124
- // Met à jour la vue pour le modèle actuellement sélectionné.
125
- const setCurrentView = (viewName) => {
126
- if (!selectedModel) return;
127
- setViewsByModel(prev => ({
128
- ...prev,
129
- [selectedModel.name]: viewName,
130
- }));
131
- };
132
-
133
- // --- MODIFICATION : Logique de changement de vue mise à jour ---
134
- const handleSwitchView = (viewName) => {
135
- if (viewName === 'table') {
136
- setCurrentView('table');
137
- return;
138
- }
139
-
140
- if (!selectedModel) {
141
- addNotification({ title: t('datalayout.selectModelFirst', 'Veuillez d\'abord sélectionner un modèle.'), status: 'warning' });
142
- return;
143
- }
144
-
145
- const modelSettings = viewSettings[selectedModel.name] || {};
146
-
147
- if (viewName === 'calendar') {
148
- if (modelSettings.calendar?.titleField && modelSettings.calendar?.startField && modelSettings.calendar?.endField) {
149
- setCurrentView('calendar');
150
- } else {
151
- setCalendarModalOpen(true);
152
- }
153
- } else if (viewName === 'kanban') {
154
- if (modelSettings.kanban?.groupByField) {
155
- setCurrentView('kanban');
156
- } else {
157
- setKanbanModalOpen(true);
158
- }
159
- }
160
- };
161
-
162
- // --- MODIFICATION : Sauvegarde dans localStorage ---
163
- const handleSaveCalendarConfig = (config) => {
164
- if (!selectedModel) return;
165
- const isConfigValid = config && config.titleField && config.startField && config.endField;
166
-
167
- if (isConfigValid) {
168
- setViewSettings(prev => ({
169
- ...prev,
170
- [selectedModel.name]: {
171
- ...(prev[selectedModel.name] || {}),
172
- calendar: config,
173
- },
174
- }));
175
- setCalendarModalOpen(false);
176
- setCurrentView('calendar');
177
- } else {
178
- // Si la configuration n'est pas valide, afficher une notification et garder la modale ouverte
179
- addNotification({
180
- title: t('datalayout.invalidConfigTitle', 'Configuration invalide'),
181
- message: t('datalayout.invalidConfigMessage', 'Veuillez vous assurer que les champs pour le titre, la date de début et la date de fin sont tous sélectionnés.'),
182
- status: 'error'
183
- });
184
- }
185
- };
186
-
187
- const handleSaveKanbanConfig = (config) => {
188
- if (!selectedModel) return;
189
- setViewSettings(prev => ({
190
- ...prev,
191
- [selectedModel.name]: {
192
- ...(prev[selectedModel.name] || {}),
193
- kanban: config,
194
- },
195
- }));
196
- setKanbanModalOpen(false);
197
- setCurrentView('kanban');
198
- };
199
-
200
- // --- MODIFICATION : Dérive les settings du modèle courant depuis l'at global ---
201
- const currentModelViewSettings = useMemo(() => {
202
- if (!selectedModel) return {};
203
- return viewSettings[selectedModel.name] || {};
204
- }, [viewSettings, selectedModel]);
205
-
206
- // --- MODIFICATION : Vérifie si les vues sont configurées pour le modèle courant ---
207
- const configuredViews = useMemo(() => {
208
- if (!selectedModel) return { calendar: false, kanban: false };
209
- const modelSettings = viewSettings[selectedModel.name] || {};
210
- return {
211
- calendar: !!modelSettings.calendar?.titleField && !!modelSettings.calendar?.startField && !!modelSettings.calendar?.endField,
212
- kanban: !!modelSettings.kanban?.groupByField,
213
- };
214
- }, [viewSettings, selectedModel]);
215
-
216
- // --- AJOUT : Logique de rendu de la vue courante ---
217
- const renderCurrentView = () => {
218
- if (!selectedModel) return null;
219
-
220
- switch (currentView) {
221
- case 'calendar':
222
- return configuredViews.calendar
223
- ? <CalendarView settings={currentModelViewSettings.calendar} onEditData={(model, data) => handleAddData(model,data)} model={selectedModel} />
224
- : <NotConfiguredPlaceholder type="calendar" onConfigure={() => setCalendarModalOpen(true)} />;
225
- case 'kanban':
226
- return configuredViews.kanban
227
- ? <KanbanView settings={currentModelViewSettings.kanban} model={selectedModel} />
228
- : <NotConfiguredPlaceholder type="kanban" onConfigure={() => setKanbanModalOpen(true)} />;
229
- case 'table':
230
- default:
231
- // Le DataTable existant est retourné par défaut
232
- return <DataTable
233
- checkedItems={checkedItems}
234
- setCheckedItems={setCheckedItems}
235
- filterValues={filterValues}
236
- setFilterValues={setFilterValues}
237
- model={selectedModel}
238
- onAddData={(model) => {
239
- mainPartRef.current.scrollIntoView({behavior: "smooth"});
240
- handleAddData(model);
241
- }}
242
- onDuplicateData={(data) => {
243
- mainPartRef.current.scrollIntoView({behavior: "smooth"});
244
- handleAddData(selectedModel, data);
245
- }}
246
- onShowAPI={() => {
247
- setAPIInfoVisible(true);
248
- setDataEditorVisible(false);
249
- setEditionMode(false);
250
- }}
251
- onEdit={(item) => {
252
- mainPartRef.current.scrollIntoView({behavior: "smooth"});
253
- setRecordToEdit(item);
254
- setFormData(item);
255
- setDataEditorVisible(true);
256
- }}
257
- onDelete={(item) => {
258
- queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, elementsPerPage, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
259
- }}
260
- />;
261
- }
262
- };
263
-
264
- const handleModelSelect = (model) => {
265
- setRelationFilters({});
266
- setCheckedItems([])
267
- setFilterValues({});
268
- if (!model) {
269
- setSelectedModel(null);
270
- return;
271
- }
272
-
273
- // Maintient la vue actuelle si elle est configurée pour le nouveau modèle, sinon revient à la vue "table"
274
- const modelSettings = viewSettings[model.name] || {};
275
- if (currentView === 'calendar' && (!modelSettings.calendar?.titleField || !modelSettings.calendar?.startField || !modelSettings.calendar?.endField)) {
276
- setCurrentView('table');
277
- } else if (currentView === 'kanban' && !modelSettings.kanban?.groupByField) {
278
- setCurrentView('table');
279
- }
280
- const dt = [];
281
- const t = [...model.fields].reduce((acc, field, index) => {
282
- if (field.type === "relation") {
283
- dt.push(field.relation);
284
- acc[field.name] = dataByModel[field.relation]?.length > 0 ? dataByModel[field.relation][0]._id : null;
285
- } else {
286
- acc[field.name] = getDefaultForType(field);
287
- }
288
- return acc;
289
- }, {});
290
- setPage(1);
291
- setFormData(t);
292
- setFilteredDatasToLoad([model.name]);
293
-
294
- setRecordToEdit(null); // Clear record to edit when model changes
295
-
296
- let tl = [];
297
- model.fields.forEach(field => {
298
- if (field.type === 'relation') {
299
- if (!tl.includes(field.relation))
300
- tl.push(field.relation);
301
- }
302
- });
303
- setDatasToLoad(tl);
304
-
305
- const cb = ({data}) => {
306
-
307
- updateRelationIds(model, data);
308
-
309
- if( datasToLoad.length === 0 ) {
310
- model.fields.forEach(field => {
311
- if (field.type === 'relation') {
312
- if (!tl.includes(field.relation))
313
- tl.push(field.relation);
314
- }
315
- });
316
- setDatasToLoad(tl);
317
- }
318
- };
319
- // add new data
320
- setOnSuccessCallbacks(cbs => {
321
- const c = {...cbs};
322
- if( !c['api/data/' + model.name + '/paged'])
323
- c['api/data/' + model.name + '/paged'] = {};
324
- c['api/data/' + model.name + '/paged'][model.name] = cb;
325
- return c;
326
- })
327
- setSelectedModel(model);
328
-
329
- gtag("event", "select_content", {
330
- content_type: "model",
331
- content_id: model.name
332
- });
333
- //queryClient.invalidateQueries(['api/data', model.name, 'page', page]);
334
- }
335
-
336
- const { me } = useAuthContext();
337
-
338
- const { addNotification } = useNotificationContext();
339
-
340
- const { mutate: insertOrUpdateMutation, isLoading} = useMutation(({formData,record}) => {
341
- const method = record ? 'PUT' : 'POST'; // Determine method based on record
342
- const url = record ? `/api/data/${record._id}` : `/api/data`; // Determine URL
343
-
344
- try {
345
- const fd = new FormData();
346
-
347
- let obj = {};
348
- for (const key in formData) {
349
- if (formData[key] !== undefined)
350
- obj[key] = formData[key];
351
- }
352
- fd.append("_data", JSON.stringify({...obj, _hash: undefined, _id: undefined}));
353
-
354
- Array.from(document.querySelectorAll('.field-file input[data-field]')).forEach(input =>{
355
- const fieldName = input.dataset['field'];
356
- if (input.files.length > 0) {
357
- Array.from(input.files).forEach((file, index) => {
358
- if( file)
359
- fd.append(`${fieldName}[${index}]`, file);
360
- });
361
- }
362
- });
363
- fd.append('model', selectedModel.name);
364
-
365
- ///fd.append("files", fd2);
366
- return fetch(`${url}?lang=${lang}&_user=${encodeURIComponent(getUserId(me))}`, {
367
- method,
368
- body: fd
369
- }).then(e => e.json());
370
-
371
- } catch (error) {
372
- console.error('Erreur lors de l\'enregistrement des données:', error);
373
- // Handle error, e.g., display error message to the user
374
- }
375
- }, {
376
- onError: (err)=>{
377
- const notificationData = {
378
- title: 'Erreur lors de l\'enregistrement des données',
379
- status: 'error'
380
- };
381
- addNotification(notificationData);
382
- },
383
- onSuccess: async (data) => {
384
-
385
- console.log('Données enregistrées:', data, selectedModel);
386
-
387
- await Event.Trigger(recordToEdit ? 'API_ADD_DATA' : 'API_ADD_DATA', "custom", "data", {
388
- model: selectedModel.name,
389
- });
390
-
391
- gtag("event", "select_content", {
392
- content_type: "edit_data",
393
- content_id: selectedModel.name
394
- });
395
-
396
- const notificationData = {
397
- title: data.success ? t('dataimporter.success', 'Données enregistrées') : t(data.error, data.error),
398
- icon: data.success ? <FaInfo /> : undefined,
399
- status: data.success ? 'completed': 'error'
400
- };
401
- addNotification(notificationData);
402
-
403
- updateRelationIds(selectedModel, formData);
404
- setDatasToLoad([...datasToLoad, selectedModel.name]);
405
- queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, elementsPerPage, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
406
-
407
- if(data.inserted) {
408
- const t = [...selectedModel.fields].reduce((acc, field, index) => {
409
- if (field.type === "relation") {
410
- acc[field.name] = field.multiple ? [] : null;
411
- } else {
412
- acc[field.name] = getDefaultForType(field);
413
- }
414
- return acc;
415
- }, {});
416
- setFormData(t)
417
- }
418
-
419
- setRelations({});
420
-
421
- }})
422
- const handleFormSubmit = async (formData, record) => { // Add record parameter
423
- insertOrUpdateMutation({formData, record})
424
- };
425
-
426
- const updateRelationIds = (model, data) => {
427
- const r = {...relationIds};
428
- let datas = !Array.isArray(data) ? [data] : data;
429
- model.fields.forEach(field => {
430
- if( field.type !== "relation")
431
- return;
432
- datas.forEach(d => {
433
- const value = d[field.name];
434
- if (field.multiple && Array.isArray(value)) {
435
- for (let i = 0; i < value.length; i++) {
436
- if (!r[field.relation]){
437
- r[field.relation] = [];
438
- }
439
- if (!r[field.relation].includes(value[i])) {
440
- r[field.relation].push(value[i]);
441
- }
442
- }
443
- } else if (!field.multiple && value) {
444
- if (!r[field.relation]){
445
- r[field.relation] = [];
446
- }
447
- if (!r[field.relation].includes(value)) {
448
- r[field.relation].push(value);
449
- }
450
- }
451
- });
452
- });
453
- setRelationIds(r);
454
- queryClient.invalidateQueries(['api/data', model.name, r]);
455
- }
456
-
457
- const deleteMutation = useMutation((selectedModels) => {
458
- return fetch('/api/data/'+checkedItems.map(m => m._id).join(',')+'?lang='+lang+'&_user='+encodeURIComponent(getUserId(me)), {
459
- method: 'DELETE', headers: {
460
- 'Content-Type': 'application/json'
461
- }
462
- }).then(e => e.json());
463
- }, { onSuccess: (data) => {
464
- if( data.success ){
465
- queryClient.invalidateQueries(['api/data', selectedModel?.name, 'page', page, elementsPerPage, pagedFilters[selectedModel?.name], pagedSort[selectedModel?.name]]);
466
- }
467
-
468
- const notificationData = {
469
- id: 'dataimporter.success',
470
- title: t('dataimporter.success', 'Données supprimées'),
471
- icon: <FaInfo />,
472
- status: 'completed'
473
- };
474
- addNotification(notificationData);
475
- }, onError:(err)=>{
476
- const notificationData = {
477
- id: 'dataimporter.error',
478
- title: err.message,
479
- status: 'error'
480
- };
481
- addNotification(notificationData);
482
- }});
483
- const handleDeletion = () => {
484
- deleteMutation.mutate();
485
- setCheckedItems([]);
486
- }
487
- const importModelsMutation = useMutation((selectedModels) => {
488
- return fetch('/api/models/import', { method: 'POST', headers: {
489
- 'Content-Type': 'application/json'
490
- },
491
- body: JSON.stringify({ models: selectedModels.map(m => m.name) })
492
- })
493
- });
494
-
495
- const handleConfigureCurrentView = () => {
496
- if (!selectedModel) return;
497
-
498
- switch (currentView) {
499
- case 'kanban':
500
- setKanbanModalOpen(true);
501
- break;
502
- case 'calendar':
503
- setCalendarModalOpen(true);
504
- break;
505
- default:
506
- // Pas de configuration pour la vue 'table'
507
- break;
508
- }
509
- };
510
-
511
- const handleAddData = (model, data)=>{
512
-
513
- if (!selectedModel || model.name !== selectedModel.name) {
514
- handleModelSelect(model);
515
- }
516
-
517
- if( data ){
518
- setFormData(data);
519
- }else {
520
- const t = model ? [...model.fields].reduce((acc, field) => {
521
- if (field.type === "relation") {
522
- acc[field.name] = field.multiple ? [] : null;
523
- } else {
524
- acc[field.name] = getDefaultForType(field);
525
- }
526
- return acc;
527
- }, {}) : [];
528
- setFormData(t)
529
- }
530
- setEditionMode(false);
531
- setDataEditorVisible(true);
532
- setAPIInfoVisible(false);
533
- gtag("event", "select_content", {
534
- content_type: "select_model",
535
- content_id: model.name
536
- });
537
- }
538
-
539
- const onImportModels = (models) => {
540
- importModelsMutation.mutateAsync(models).then(e => {
541
- queryClient.invalidateQueries('api/models');
542
- });
543
- setImportModalVisible(false);
544
- }
545
-
546
- useEffect(() => {
547
- if( showDataEditor && mainPartRef.current ){
548
-
549
- }
550
- }, [showDataEditor,mainPartRef.current]);
551
-
552
-
553
- const [currentProfile, setCurrentProfile] = useLocalStorage('profile', null);
554
- const {isTourOpen, setIsTourOpen, currentTourSteps, allTourSteps, setTourStepIndex, setCurrentTourSteps, currentTour,setCurrentTour, addLaunchedTour} = useUI();
555
-
556
- const startTour = () => {
557
- setIsTourOpen(true);
558
- };
559
-
560
- const closeTour = (completedTourName) => {
561
- // On génère le nom du tour de démo pour le comparer
562
- // This function is called when ANY tour is closed.
563
- // It needs to persist the fact that the tour has been seen.
564
-
565
- // 1. Add the tour's unique name to the list of launched tours.
566
- // This list is managed by the UI context and stored in localStorage.
567
- if (addLaunchedTour) { // Check if the function exists to be safe
568
- addLaunchedTour(completedTourName);
569
- }
570
-
571
- // 2. Close the tour's UI.
572
- setIsTourOpen(false);
573
-
574
- // 3. Specific logic for the very first "demo" tour:
575
- // We also set the user's profile to mark that they are no longer a brand new user.
576
- // This prevents the demo tour from trying to launch on every page load.
577
- const demoTourName = `tour_${getObjectHash({steps: allTourSteps.demo || []})}`;
578
-
579
- // Si le tour qui vient de se terminer est le tour de démo
580
- if (completedTourName === demoTourName) {
581
-
582
- setIsTourOpen(false);
583
- setCurrentProfile({ lastSeen: new Date().toISOString() });
584
- }
585
- };
586
-
587
- useEffect(() => {
588
- if( !currentProfile ){
589
- setIsTourOpen(true);
590
- }
591
- }, [currentProfile])
592
-
593
- useEffect(() => {
594
- // Si un modèle est sélectionné...
595
- if (selectedModel) {
596
- // Récupérer les étapes du tour "datapacks"
597
- const datapacksSteps = allTourSteps.guides;
598
- // S'assurer que ce tour existe et a des étapes
599
- if (datapacksSteps && datapacksSteps.length > 0) {
600
- // 1. Définir les étapes du tour qui doit être affiché
601
- setCurrentTourSteps(datapacksSteps);
602
- // 2. Calculer le nom unique du tour et le définir comme tour courant
603
- // C'est l'clé qui manquait.
604
- const tourName = `tour_${getObjectHash({ steps: datapacksSteps })}`;
605
- setCurrentTour(tourName);
606
- // 3. Activer l'affichage du composant de tour
607
- setIsTourOpen(true);
608
- }
609
- }
610
- }, [selectedModel, allTourSteps]); // Ajout de allTourSteps aux dépendances pour la bonne pratique
611
-
612
- return (
613
- <>
614
- <>{/^demo[0-9]{1,2}$/.test(me.username) && (
615
- <TourSpotlight
616
- name={"tour_"+getObjectHash({steps:currentTourSteps})}
617
- steps={currentTourSteps}
618
- isOpen={isTourOpen}
619
- onClose={closeTour}
620
- />)}</>
621
- <div className="datalayout flex flex-start">
622
- <ModelList tourSteps={currentTourSteps}
623
- onAPIInfo={(model) => {
624
- setAPIInfoVisible(true);
625
- setDataEditorVisible(false);
626
- setEditionMode(false);
627
- setSelectedModel(model);
628
- gtag("event", "select_content", {
629
- content_type: "api",
630
- content_id: model.name
631
- });
632
- }} onImportModel={() => {
633
- setImportModalVisible(true);
634
- }} onCreateModel={() => {
635
- setSelectedModel(null);
636
- setAPIInfoVisible(false);
637
- setDataEditorVisible(false);
638
- setEditionMode(true);
639
- mainPartRef.current.scrollIntoView({behavior: 'smooth'});
640
- gtag("event", "select_content", {
641
- content_type: "create_model",
642
- });
643
- }} onEditModel={(model) => {
644
- handleModelSelect(model);
645
- setDataEditorVisible(false);
646
- setAPIInfoVisible(false);
647
- setEditionMode(true);
648
- mainPartRef.current.scrollIntoView({behavior: 'smooth'});
649
- gtag("event", "select_content", {
650
- content_type: "edit_model",
651
- content_id: model.name
652
- });
653
- }} onModelSelect={(model) => {
654
- handleModelSelect(model);
655
- setDataEditorVisible(false);
656
- setEditionMode(false);
657
- setAPIInfoVisible(false);
658
- mainPartRef.current.scrollIntoView({behavior: 'smooth'});
659
- gtag("event", "select_content", {
660
- content_type: "select_model",
661
- content_id: model.name
662
- });
663
- }} onNewData={(model) => {
664
- mainPartRef.current.scrollIntoView({behavior: 'smooth'});
665
- handleAddData(model);
666
- }}/>
667
- {(editionMode) && (
668
- <ModelCreator ref={modelCreatorRef} onModelGenerated={() =>{
669
- modelCreatorRef.current.scrollIntoView({behavior: 'smooth'});
670
- }} initialModel={selectedModel} onModelSaved={(model) => {
671
- setRefreshTime(new Date().getTime());
672
- handleModelSelect(model);
673
- }}/>)}
674
-
675
- <div className="hidden-anchor" ref={mainPartRef}></div>
676
-
677
- {showDataEditor && (<DataEditor
678
- key={selectedModel?.name}
679
- isLoading={isLoading}
680
- model={selectedModel}
681
- formData={formData}
682
- setFormData={setFormData}
683
- refreshTime={refreshTime}
684
- onSubmit={handleFormSubmit}
685
- setRecord={setRecordToEdit}
686
- record={recordToEdit} // Pass record to edit to DataEditor
687
- />)}
688
-
689
-
690
- {selectedModel && showAPIInfo && <APIInfo/>}
691
- {selectedModel && !showAPIInfo && !generatedModels.some(g => g.name === selectedModel?.name) && (<div className="datas">
692
-
693
- {<ViewSwitcher
694
- currentView={currentView}
695
- onViewChange={handleSwitchView}
696
- configuredViews={configuredViews}
697
- onConfigureView={handleConfigureCurrentView}
698
- />}
699
- <h2>{t(`model_${selectedModel?.name}`, selectedModel?.name)} <>({countByModel?.[selectedModel?.name]})</></h2>
700
-
701
-
702
- {renderCurrentView()}
703
-
704
- {isDataLoaded && currentView === 'table' && (<>
705
- {selectedModel && (<Pagination showElementsPerPage={true} onChange={page => {
706
- setPage(page);
707
- setCheckedItems([]);
708
- gtag("event", "select_content", {
709
- content_type: "change_page",
710
- content_id: page
711
- });
712
- queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
713
- }} page={page} setPage={setPage} totalCount={countByModel[selectedModel.name]}
714
- hasPreviousNext={true} visibleItemsCount={5}
715
- elementsPerPage={elementsPerPage}/>)}
716
- <div className="actions flex">
717
- <Button onClick={() => {
718
- setCheckedItems(paginatedDataByModel[selectedModel.name]);
719
- }}><Trans i18nKey={"datatable.selectAll"}>Tout sélectionner</Trans></Button>
720
- <Button onClick={handleDeletion} disabled={!checkedItems?.length}><Trans
721
- i18nKey={"datatable.deleteSelection"}>Supprimer la sélection</Trans></Button>
722
- </div>
723
- </>)}
724
- </div>)}
725
- </div>
726
-
727
- <DialogProvider>
728
- {importModalVisible && (<Dialog onClose={() => {
729
- setImportModalVisible(false)
730
- }} isModal={true} isClosable={true}>
731
- <ModelImporter onImport={onImportModels}/>
732
- </Dialog>)}
733
-
734
- <CalendarConfigModal
735
- isOpen={isCalendarModalOpen}
736
- onClose={() => setCalendarModalOpen(false)}
737
- onSave={handleSaveCalendarConfig}
738
- modelFields={selectedModel?.fields ||[]}
739
- initialSettings={currentModelViewSettings.calendar}
740
- />
741
-
742
- <KanbanConfigModal
743
- isOpen={isKanbanModalOpen}
744
- onClose={() => setKanbanModalOpen(false)}
745
- onSave={handleSaveKanbanConfig}
746
- model={selectedModel}
747
- modelFields={selectedModel?.fields||[]}
748
- initialSettings={currentModelViewSettings.kanban}
749
- />
750
- </DialogProvider>
751
- </>
752
- );
753
- }
754
-
755
-
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 ModelCreator from "./ModelCreator.jsx";
6
+ import {useModelContext} from "./contexts/ModelContext.jsx";
7
+ import {Dialog, DialogProvider} from "./Dialog.jsx";
8
+ import {Pagination} from "./Pagination.jsx";
9
+ import {Event} from "../../src/events.js";
10
+
11
+ import {
12
+ FaBook,
13
+ FaBoxOpen, FaDatabase,
14
+ FaEye, FaFileImport,
15
+ FaFilter, FaInfo, FaPlus,
16
+ } from "react-icons/fa";
17
+ import {getDefaultForType, getUserHash, getUserId} from "../../src/data.js";
18
+ import {Trans, useTranslation} from "react-i18next";
19
+
20
+ import {getObjectHash} from "../../src/core.js";
21
+ import Button from "./Button.jsx";
22
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
23
+ import APIInfo from "./APIInfo.jsx";
24
+ import {useNotificationContext} from "./NotificationProvider.jsx";
25
+ import {ModelImporter} from "./ModelImporter.jsx";
26
+ import {DataTable} from "./DataTable.jsx";
27
+ import {ModelList} from "./ModelList.jsx";
28
+ import {DataEditor} from "./DataEditor.jsx";
29
+ import TourSpotlight from "./TourSpotlight.jsx";
30
+ import useLocalStorage from "./hooks/useLocalStorage.js";
31
+ import {useUI} from "./contexts/UIContext.jsx";
32
+ import {useTutorials} from "./hooks/useTutorials.jsx";
33
+ import ViewSwitcher from "./ViewSwitcher.jsx";
34
+ import KanbanConfigModal from "./KanbanConfigModal.jsx";
35
+ import CalendarConfigModal from "./CalendarConfigModal.jsx";
36
+ import KanbanView from "./KanbanView.jsx";
37
+ import CalendarView from "./CalendarView.jsx";
38
+ import {useLocation, useParams, useSearchParams} from "react-router-dom";
39
+ import { useNavigate } from "react-router-dom";
40
+ import {Tooltip} from "react-tooltip";
41
+ import PackGallery from "./PackGallery.jsx";
42
+ import TutorialsMenu from "./TutorialsMenu.jsx";
43
+ import {FaBookAtlas} from "react-icons/fa6";
44
+ import {AssistantChat, NotificationList} from "../index.js";
45
+
46
+ import "./DataLayout.scss"
47
+
48
+ const NotConfiguredPlaceholder = ({ type, onConfigure }) => (
49
+ <div className="p-4 border border-dashed rounded-md mt-4 text-center bg-gray-50">
50
+ <h4><Trans i18nKey="dataview.notConfiguredTitle" values={{ type }}>Vue {{type}} non configurée</Trans></h4>
51
+ <p className="text-sm text-gray-600"><Trans i18nKey="dataview.notConfiguredText">Veuillez configurer cette vue pour l'utiliser.</Trans></p>
52
+ <Button onClick={onConfigure} className="mt-2">
53
+ <Trans i18nKey="dataview.configureButton" values={{ type }}>Configurer {{type}}</Trans>
54
+ </Button>
55
+ </div>
56
+ );
57
+
58
+ function DataLayout({refreshUI}) {
59
+ const [ searchParams, setSearchParams ] = useSearchParams();
60
+ const [viewSettings, setViewSettings] = useLocalStorage('viewSettings', {});
61
+
62
+ const [isCalendarModalOpen, setCalendarModalOpen] = useState(false);
63
+ const [isKanbanModalOpen, setKanbanModalOpen] = useState(false);
64
+ const [showPackGallery, setShowPackGallery] = useState(false);
65
+
66
+ const { triggerTutorialCheck } = useTutorials();
67
+ const { t, i18n } = useTranslation();
68
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
69
+
70
+ // Stocke la vue sélectionnée pour chaque modèle. Ex: { "contacts": "calendar", "tasks": "kanban" }
71
+ const [viewsByModel, setViewsByModel] = useLocalStorage('dataLayout_viewsByModel', {});
72
+
73
+ const [filterValues, setFilterValues] = useState({});
74
+ const { dataByModel,paginatedDataByModel,
75
+ setRelationFilters, setSelectedModel, selectedModel,
76
+ setFilteredDatasToLoad,
77
+ setRelationIds,
78
+ setOnSuccessCallbacks,
79
+ datasToLoad,
80
+ setDatasToLoad,page, setPage, countByModel, relationIds,
81
+ pagedFilters, pagedSort,
82
+ elementsPerPage,
83
+ setRelations,
84
+ generatedModels,
85
+ models
86
+ } = useModelContext(); // Utilisez le contexte
87
+ const queryClient = useQueryClient();
88
+
89
+ const isDataLoaded = true;
90
+ const [checkedItems, setCheckedItems] = useState([])
91
+
92
+ const [refreshTime, setRefreshTime] = useState(0);
93
+ const [formData, setFormData] = useState({});
94
+ const [recordToEdit, setRecordToEdit] = useState(null); // New state for record to edit
95
+
96
+ const mainPartRef = useRef();
97
+ const modelCreatorRef = useRef();
98
+
99
+ const [tutorialDialogVisible, setTutorialDialogVisible] = useState(false);
100
+ const [importModalVisible, setImportModalVisible] = useState(false);
101
+ const [editionMode, setEditionMode] = useState(false);
102
+ const [showDataEditor, setDataEditorVisible] = useState(false);
103
+ const [showAPIInfo, setAPIInfoVisible] = useState(false);
104
+ const nav = useNavigate();
105
+ const mod = searchParams.get('model');
106
+ const loc = useLocation();
107
+ useEffect(() =>{
108
+ if (selectedModel?.name) {
109
+ nav('/user/' + getUserHash(me) + '/?model=' + selectedModel?.name);
110
+ }
111
+ }, [selectedModel?.name])
112
+
113
+
114
+ useEffect(() => {
115
+ setSelectedModel(null)
116
+ setDataEditorVisible(false);
117
+ setAPIInfoVisible(false);
118
+ setEditionMode(true);
119
+ }, []);
120
+
121
+ useEffect(() =>{
122
+ const m = models.find(f => f.name === mod);
123
+ setSelectedModel(m);
124
+ setEditionMode(!m);
125
+ }, [mod, models, searchParams])
126
+
127
+
128
+ // La vue courante est dérivée du modèle sélectionné et des préférences stockées.
129
+ const currentView = useMemo(() => {
130
+ if (!selectedModel) return 'table';
131
+ return viewsByModel[selectedModel.name] || 'table';
132
+ }, [selectedModel, viewsByModel]);
133
+
134
+ // Met à jour la vue pour le modèle actuellement sélectionné.
135
+ const setCurrentView = (viewName) => {
136
+ if (!selectedModel) return;
137
+ setViewsByModel(prev => ({
138
+ ...prev,
139
+ [selectedModel.name]: viewName,
140
+ }));
141
+ };
142
+
143
+ // --- MODIFICATION : Logique de changement de vue mise à jour ---
144
+ const handleSwitchView = (viewName) => {
145
+ if (viewName === 'table') {
146
+ setCurrentView('table');
147
+ return;
148
+ }
149
+
150
+ if (!selectedModel) {
151
+ addNotification({ title: t('datalayout.selectModelFirst', 'Veuillez d\'abord sélectionner un modèle.'), status: 'warning' });
152
+ return;
153
+ }
154
+
155
+ const modelSettings = viewSettings[selectedModel.name] || {};
156
+
157
+ if (viewName === 'calendar') {
158
+ if (modelSettings.calendar?.titleField && modelSettings.calendar?.startField && modelSettings.calendar?.endField) {
159
+ setCurrentView('calendar');
160
+ } else {
161
+ setCalendarModalOpen(true);
162
+ }
163
+ } else if (viewName === 'kanban') {
164
+ if (modelSettings.kanban?.groupByField) {
165
+ setCurrentView('kanban');
166
+ } else {
167
+ setKanbanModalOpen(true);
168
+ }
169
+ }
170
+ };
171
+
172
+ // --- MODIFICATION : Sauvegarde dans localStorage ---
173
+ const handleSaveCalendarConfig = (config) => {
174
+ if (!selectedModel) return;
175
+ const isConfigValid = config && config.titleField && config.startField && config.endField;
176
+
177
+ if (isConfigValid) {
178
+ setViewSettings(prev => ({
179
+ ...prev,
180
+ [selectedModel.name]: {
181
+ ...(prev[selectedModel.name] || {}),
182
+ calendar: config,
183
+ },
184
+ }));
185
+ setCalendarModalOpen(false);
186
+ setCurrentView('calendar');
187
+ } else {
188
+ // Si la configuration n'est pas valide, afficher une notification et garder la modale ouverte
189
+ addNotification({
190
+ title: t('datalayout.invalidConfigTitle', 'Configuration invalide'),
191
+ message: t('datalayout.invalidConfigMessage', 'Veuillez vous assurer que les champs pour le titre, la date de début et la date de fin sont tous sélectionnés.'),
192
+ status: 'error'
193
+ });
194
+ }
195
+ };
196
+
197
+ const handleSaveKanbanConfig = (config) => {
198
+ if (!selectedModel) return;
199
+ setViewSettings(prev => ({
200
+ ...prev,
201
+ [selectedModel.name]: {
202
+ ...(prev[selectedModel.name] || {}),
203
+ kanban: config,
204
+ },
205
+ }));
206
+ setKanbanModalOpen(false);
207
+ setCurrentView('kanban');
208
+ };
209
+
210
+ // --- MODIFICATION : Dérive les settings du modèle courant depuis l'at global ---
211
+ const currentModelViewSettings = useMemo(() => {
212
+ if (!selectedModel) return {};
213
+ return viewSettings[selectedModel.name] || {};
214
+ }, [viewSettings, selectedModel]);
215
+
216
+ // --- MODIFICATION : Vérifie si les vues sont configurées pour le modèle courant ---
217
+ const configuredViews = useMemo(() => {
218
+ if (!selectedModel) return { calendar: false, kanban: false };
219
+ const modelSettings = viewSettings[selectedModel.name] || {};
220
+ return {
221
+ calendar: !!modelSettings.calendar?.titleField && !!modelSettings.calendar?.startField && !!modelSettings.calendar?.endField,
222
+ kanban: !!modelSettings.kanban?.groupByField,
223
+ };
224
+ }, [viewSettings, selectedModel]);
225
+
226
+ // --- AJOUT : Logique de rendu de la vue courante ---
227
+ const renderCurrentView = () => {
228
+ if (!selectedModel) return null;
229
+
230
+ switch (currentView) {
231
+ case 'calendar':
232
+ return configuredViews.calendar
233
+ ? <CalendarView settings={currentModelViewSettings.calendar} onEditData={(model, data) => handleAddData(model,data)} model={selectedModel} />
234
+ : <NotConfiguredPlaceholder type="calendar" onConfigure={() => setCalendarModalOpen(true)} />;
235
+ case 'kanban':
236
+ return configuredViews.kanban
237
+ ? <KanbanView settings={currentModelViewSettings.kanban} model={selectedModel} />
238
+ : <NotConfiguredPlaceholder type="kanban" onConfigure={() => setKanbanModalOpen(true)} />;
239
+ case 'table':
240
+ default:
241
+ // Le DataTable existant est retourné par défaut
242
+ return <DataTable
243
+ checkedItems={checkedItems}
244
+ setCheckedItems={setCheckedItems}
245
+ filterValues={filterValues}
246
+ setFilterValues={setFilterValues}
247
+ model={selectedModel}
248
+ onAddData={(model) => {
249
+ mainPartRef.current.scrollIntoView({behavior: "smooth"});
250
+ handleAddData(model);
251
+ }}
252
+ onDuplicateData={(data) => {
253
+ mainPartRef.current.scrollIntoView({behavior: "smooth"});
254
+ handleAddData(selectedModel, data);
255
+ }}
256
+ onEdit={(item) => {
257
+ mainPartRef.current.scrollIntoView({behavior: "smooth"});
258
+ setRecordToEdit(item);
259
+ setFormData(item);
260
+ setDataEditorVisible(true);
261
+ }}
262
+ onDelete={(item) => {
263
+ queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, elementsPerPage, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
264
+ }}
265
+ />;
266
+ }
267
+ };
268
+
269
+ const handleModelSelect = (model) => {
270
+ setRelationFilters({});
271
+ setCheckedItems([])
272
+ setFilterValues({});
273
+ if (!model) {
274
+ setSelectedModel(null);
275
+ return;
276
+ }
277
+
278
+ // Maintient la vue actuelle si elle est configurée pour le nouveau modèle, sinon revient à la vue "table"
279
+ const modelSettings = viewSettings[model.name] || {};
280
+ if (currentView === 'calendar' && (!modelSettings.calendar?.titleField || !modelSettings.calendar?.startField || !modelSettings.calendar?.endField)) {
281
+ setCurrentView('table');
282
+ } else if (currentView === 'kanban' && !modelSettings.kanban?.groupByField) {
283
+ setCurrentView('table');
284
+ }
285
+ const dt = [];
286
+ const t = [...model.fields].reduce((acc, field, index) => {
287
+ if (field.type === "relation") {
288
+ dt.push(field.relation);
289
+ acc[field.name] = dataByModel[field.relation]?.length > 0 ? dataByModel[field.relation][0]._id : null;
290
+ } else {
291
+ acc[field.name] = getDefaultForType(field);
292
+ }
293
+ return acc;
294
+ }, {});
295
+ setPage(1);
296
+ setFormData(t);
297
+ setFilteredDatasToLoad([model.name]);
298
+
299
+ setRecordToEdit(null); // Clear record to edit when model changes
300
+
301
+ let tl = [];
302
+ model.fields.forEach(field => {
303
+ if (field.type === 'relation') {
304
+ if (!tl.includes(field.relation))
305
+ tl.push(field.relation);
306
+ }
307
+ });
308
+ setDatasToLoad(tl);
309
+
310
+ const cb = ({data}) => {
311
+
312
+ updateRelationIds(model, data);
313
+
314
+ if( datasToLoad.length === 0 ) {
315
+ model.fields.forEach(field => {
316
+ if (field.type === 'relation') {
317
+ if (!tl.includes(field.relation))
318
+ tl.push(field.relation);
319
+ }
320
+ });
321
+ setDatasToLoad(tl);
322
+ }
323
+ };
324
+ // add new data
325
+ setOnSuccessCallbacks(cbs => {
326
+ const c = {...cbs};
327
+ if( !c['api/data/' + model.name + '/paged'])
328
+ c['api/data/' + model.name + '/paged'] = {};
329
+ c['api/data/' + model.name + '/paged'][model.name] = cb;
330
+ return c;
331
+ })
332
+ setSelectedModel(model);
333
+
334
+ gtag("event", "select_content", {
335
+ content_type: "model",
336
+ content_id: model.name
337
+ });
338
+ //queryClient.invalidateQueries(['api/data', model.name, 'page', page]);
339
+ }
340
+
341
+ const { me } = useAuthContext();
342
+
343
+ const { addNotification } = useNotificationContext();
344
+
345
+ const { mutate: insertOrUpdateMutation, isLoading} = useMutation(({formData,record}) => {
346
+ const method = record ? 'PUT' : 'POST'; // Determine method based on record
347
+ const url = record ? `/api/data/${record._id}` : `/api/data`; // Determine URL
348
+
349
+ try {
350
+ const fd = new FormData();
351
+
352
+ let obj = {};
353
+ for (const key in formData) {
354
+ if (formData[key] !== undefined)
355
+ obj[key] = formData[key];
356
+ }
357
+ Array.from(document.querySelectorAll('.field-file input[data-field]')).forEach(input =>{
358
+ const fieldName = input.dataset['field'];
359
+ for (let x = 0; x < input.files.length; x++) {
360
+ fd.append(`${fieldName}[${x}]`, input.files[x]);
361
+ }
362
+ obj[fieldName] = null;
363
+ });
364
+ fd.append("_data", JSON.stringify({...obj, _hash: undefined, _id: undefined}));
365
+ fd.append('model', selectedModel.name);
366
+
367
+ ///fd.append("files", fd2);
368
+ return fetch(`${url}?lang=${lang}&_user=${encodeURIComponent(getUserId(me))}`, {
369
+ method,
370
+ body: fd
371
+ }).then(e => e.json());
372
+
373
+ } catch (error) {
374
+ console.error('Erreur lors de l\'enregistrement des données:', error);
375
+ // Handle error, e.g., display error message to the user
376
+ }
377
+ }, {
378
+ onError: (err)=>{
379
+ const notificationData = {
380
+ title: 'Erreur lors de l\'enregistrement des données',
381
+ status: 'error'
382
+ };
383
+ addNotification(notificationData);
384
+ },
385
+ onSuccess: async (data) => {
386
+
387
+ console.log('Données enregistrées:', data, selectedModel);
388
+
389
+ await Event.Trigger(recordToEdit ? 'API_ADD_DATA' : 'API_ADD_DATA', "custom", "data", {
390
+ model: selectedModel.name,
391
+ });
392
+
393
+ gtag("event", "select_content", {
394
+ content_type: "edit_data",
395
+ content_id: selectedModel.name
396
+ });
397
+
398
+ const notificationData = {
399
+ title: data.success ? t('dataimporter.success', 'Données enregistrées') : t(data.error, data.error),
400
+ icon: data.success ? <FaInfo /> : undefined,
401
+ status: data.success ? 'completed': 'error'
402
+ };
403
+ addNotification(notificationData);
404
+
405
+ updateRelationIds(selectedModel, formData);
406
+ setDatasToLoad([...datasToLoad, selectedModel.name]);
407
+ queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, elementsPerPage, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
408
+
409
+ if(data.inserted) {
410
+ const t = [...selectedModel.fields].reduce((acc, field, index) => {
411
+ if (field.type === "relation") {
412
+ acc[field.name] = field.multiple ? [] : null;
413
+ } else {
414
+ acc[field.name] = getDefaultForType(field);
415
+ }
416
+ return acc;
417
+ }, {});
418
+ setFormData(t)
419
+ }
420
+
421
+ setRelations({});
422
+
423
+ }})
424
+ const handleFormSubmit = async (formData, record) => { // Add record parameter
425
+ insertOrUpdateMutation({formData, record})
426
+ };
427
+
428
+ const updateRelationIds = (model, data) => {
429
+ const r = {...relationIds};
430
+ let datas = !Array.isArray(data) ? [data] : data;
431
+ model.fields.forEach(field => {
432
+ if( field.type !== "relation")
433
+ return;
434
+ datas.forEach(d => {
435
+ const value = d[field.name];
436
+ if (field.multiple && Array.isArray(value)) {
437
+ for (let i = 0; i < value.length; i++) {
438
+ if (!r[field.relation]){
439
+ r[field.relation] = [];
440
+ }
441
+ if (!r[field.relation].includes(value[i])) {
442
+ r[field.relation].push(value[i]);
443
+ }
444
+ }
445
+ } else if (!field.multiple && value) {
446
+ if (!r[field.relation]){
447
+ r[field.relation] = [];
448
+ }
449
+ if (!r[field.relation].includes(value)) {
450
+ r[field.relation].push(value);
451
+ }
452
+ }
453
+ });
454
+ });
455
+ setRelationIds(r);
456
+ queryClient.invalidateQueries(['api/data', model.name, r]);
457
+ }
458
+
459
+ const deleteMutation = useMutation((selectedModels) => {
460
+ return fetch('/api/data/'+checkedItems.map(m => m._id).join(',')+'?lang='+lang+'&_user='+encodeURIComponent(getUserId(me)), {
461
+ method: 'DELETE', headers: {
462
+ 'Content-Type': 'application/json'
463
+ }
464
+ }).then(e => e.json());
465
+ }, { onSuccess: (data) => {
466
+ if( data.success ){
467
+ queryClient.invalidateQueries(['api/data', selectedModel?.name, 'page', page, elementsPerPage, pagedFilters[selectedModel?.name], pagedSort[selectedModel?.name]]);
468
+ }
469
+
470
+ const notificationData = {
471
+ id: 'dataimporter.success',
472
+ title: t('dataimporter.success', 'Données supprimées'),
473
+ icon: <FaInfo />,
474
+ status: 'completed'
475
+ };
476
+ addNotification(notificationData);
477
+ }, onError:(err)=>{
478
+ const notificationData = {
479
+ id: 'dataimporter.error',
480
+ title: err.message,
481
+ status: 'error'
482
+ };
483
+ addNotification(notificationData);
484
+ }});
485
+ const handleDeletion = () => {
486
+ deleteMutation.mutate();
487
+ setCheckedItems([]);
488
+ }
489
+ const importModelsMutation = useMutation((selectedModels) => {
490
+ return fetch('/api/models/import', { method: 'POST', headers: {
491
+ 'Content-Type': 'application/json'
492
+ },
493
+ body: JSON.stringify({ models: selectedModels.map(m => m.name) })
494
+ })
495
+ });
496
+
497
+ const handleConfigureCurrentView = () => {
498
+ if (!selectedModel) return;
499
+
500
+ switch (currentView) {
501
+ case 'kanban':
502
+ setKanbanModalOpen(true);
503
+ break;
504
+ case 'calendar':
505
+ setCalendarModalOpen(true);
506
+ break;
507
+ default:
508
+ // Pas de configuration pour la vue 'table'
509
+ break;
510
+ }
511
+ };
512
+
513
+ const handleAddData = (model, data)=>{
514
+
515
+ if (!selectedModel || model.name !== selectedModel.name) {
516
+ handleModelSelect(model);
517
+ }
518
+
519
+ if( data ){
520
+ setFormData(data);
521
+ }else {
522
+ const t = model ? [...model.fields].reduce((acc, field) => {
523
+ if (field.type === "relation") {
524
+ acc[field.name] = field.multiple ? [] : null;
525
+ } else {
526
+ acc[field.name] = getDefaultForType(field);
527
+ }
528
+ return acc;
529
+ }, {}) : [];
530
+ setFormData(t)
531
+ }
532
+ setEditionMode(false);
533
+ setDataEditorVisible(true);
534
+ setAPIInfoVisible(false);
535
+ gtag("event", "select_content", {
536
+ content_type: "select_model",
537
+ content_id: model.name
538
+ });
539
+ }
540
+
541
+ const onImportModels = (models) => {
542
+ importModelsMutation.mutateAsync(models).then(e => {
543
+ queryClient.invalidateQueries('api/models');
544
+ });
545
+ setImportModalVisible(false);
546
+ }
547
+
548
+ useEffect(() => {
549
+ if( showDataEditor && mainPartRef.current ){
550
+
551
+ }
552
+ }, [showDataEditor,mainPartRef.current]);
553
+
554
+
555
+ const [currentProfile, setCurrentProfile] = useLocalStorage('profile', null);
556
+ const {isTourOpen, setIsTourOpen, currentTourSteps, allTourSteps, setTourStepIndex, setCurrentTourSteps, currentTour,setCurrentTour, addLaunchedTour, assistantConfig, setAssistantConfig} = useUI();
557
+
558
+ const startTour = () => {
559
+ setIsTourOpen(true);
560
+ };
561
+
562
+ const closeTour = (completedTourName) => {
563
+ // On génère le nom du tour de démo pour le comparer
564
+ // This function is called when ANY tour is closed.
565
+ // It needs to persist the fact that the tour has been seen.
566
+
567
+ // 1. Add the tour's unique name to the list of launched tours.
568
+ // This list is managed by the UI context and stored in localStorage.
569
+ if (addLaunchedTour) { // Check if the function exists to be safe
570
+ addLaunchedTour(completedTourName);
571
+ }
572
+
573
+ // 2. Close the tour's UI.
574
+ setIsTourOpen(false);
575
+
576
+ // 3. Specific logic for the very first "demo" tour:
577
+ // We also set the user's profile to mark that they are no longer a brand new user.
578
+ // This prevents the demo tour from trying to launch on every page load.
579
+ const demoTourName = `tour_${getObjectHash({steps: allTourSteps.demo || []})}`;
580
+
581
+ // Si le tour qui vient de se terminer est le tour de démo
582
+ if (completedTourName === demoTourName) {
583
+
584
+ setIsTourOpen(false);
585
+ setCurrentProfile({ lastSeen: new Date().toISOString() });
586
+ }
587
+ };
588
+
589
+ useEffect(() => {
590
+ if( !currentProfile ){
591
+ setIsTourOpen(true);
592
+ }
593
+ }, [currentProfile])
594
+
595
+ useEffect(() => {
596
+ // Si un modèle est sélectionné...
597
+ if (selectedModel) {
598
+ // Récupérer les étapes du tour "datapacks"
599
+ const datapacksSteps = allTourSteps.guides;
600
+ // S'assurer que ce tour existe et a des étapes
601
+ if (datapacksSteps && datapacksSteps.length > 0) {
602
+ // 1. Définir les étapes du tour qui doit être affiché
603
+ setCurrentTourSteps(datapacksSteps);
604
+ // 2. Calculer le nom unique du tour et le définir comme tour courant
605
+ // C'est l'clé qui manquait.
606
+ const tourName = `tour_${getObjectHash({ steps: datapacksSteps })}`;
607
+ setCurrentTour(tourName);
608
+ // 3. Activer l'affichage du composant de tour
609
+ setIsTourOpen(true);
610
+ }
611
+ }
612
+ }, [selectedModel, allTourSteps]); // Ajout de allTourSteps aux dépendances pour la bonne pratique
613
+
614
+ const handleShowTutorialMenu = () => {
615
+ setTutorialDialogVisible(!tutorialDialogVisible);
616
+ }
617
+ return (
618
+ <>
619
+ <Tooltip id="tooltipField" />
620
+ <>{/^demo[0-9]{1,2}$/.test(me.username) && (
621
+ <TourSpotlight
622
+ name={"tour_"+getObjectHash({steps:currentTourSteps})}
623
+ steps={currentTourSteps}
624
+ isOpen={isTourOpen}
625
+ onClose={closeTour}
626
+ />)}</>
627
+ <div className="flex actions">
628
+
629
+ {<ViewSwitcher
630
+ currentView={currentView}
631
+ onViewChange={handleSwitchView}
632
+ configuredViews={configuredViews}
633
+ onConfigureView={handleConfigureCurrentView}
634
+ />}
635
+ <Button onClick={() => {
636
+ setImportModalVisible(true);
637
+ }} className="btn tourStep-import-model"><FaFileImport/><Trans
638
+ i18nKey="btns.importModels">Modèles</Trans></Button>
639
+ <Button onClick={() => {
640
+ setShowPackGallery(true);
641
+ }} className="btn tourStep-import-pack"><FaBoxOpen/><Trans
642
+ i18nKey="btns.importPacks">Packs</Trans></Button>
643
+ <Button className={"tourStep-tutorials btn"} onClick={handleShowTutorialMenu} title={t("btns.showTutos")}><FaBookAtlas/><Trans
644
+ i18nKey="btns.showTutos">Tutoriels</Trans></Button>
645
+
646
+ <DialogProvider>
647
+ {tutorialDialogVisible && (
648
+ <Dialog isClosable={true} isModal={true} onClose={() => setTutorialDialogVisible(false)}>
649
+ <TutorialsMenu />
650
+ </Dialog>
651
+ )}
652
+ </DialogProvider>
653
+ <Button className="btn" onClick={() => {
654
+ setAPIInfoVisible(true);
655
+ setDataEditorVisible(false);
656
+ setEditionMode(false);
657
+ }}><FaBook/> {t('btns.api', 'API')}</Button>
658
+ </div>
659
+
660
+ <div className="datalayout flex flex-start">
661
+ <ModelList tourSteps={currentTourSteps}
662
+ onAPIInfo={(model) => {
663
+ setAPIInfoVisible(true);
664
+ setDataEditorVisible(false);
665
+ setEditionMode(false);
666
+ setSelectedModel(model);
667
+ gtag("event", "select_content", {
668
+ content_type: "api",
669
+ content_id: model.name
670
+ });
671
+ }} onImportModel={() => {
672
+ setImportModalVisible(true);
673
+ }} onCreateModel={() => {
674
+ setSelectedModel(null);
675
+ setAPIInfoVisible(false);
676
+ setDataEditorVisible(false);
677
+ setEditionMode(true);
678
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
679
+ gtag("event", "select_content", {
680
+ content_type: "create_model",
681
+ });
682
+ }} onEditModel={(model) => {
683
+ handleModelSelect(model);
684
+ setDataEditorVisible(false);
685
+ setAPIInfoVisible(false);
686
+ setEditionMode(true);
687
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
688
+ gtag("event", "select_content", {
689
+ content_type: "edit_model",
690
+ content_id: model.name
691
+ });
692
+ }} onModelSelect={(model) => {
693
+ handleModelSelect(model);
694
+ setDataEditorVisible(false);
695
+ setEditionMode(false);
696
+ setAPIInfoVisible(false);
697
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
698
+ gtag("event", "select_content", {
699
+ content_type: "select_model",
700
+ content_id: model.name
701
+ });
702
+ }} onImportPack={() => {
703
+ setShowPackGallery(true);
704
+ }} onNewData={(model) => {
705
+ mainPartRef.current.scrollIntoView({behavior: 'smooth'});
706
+ handleAddData(model);
707
+ }}/>
708
+ {(editionMode) && (
709
+ <ModelCreator ref={modelCreatorRef} onModelGenerated={() =>{
710
+ modelCreatorRef.current.scrollIntoView({behavior: 'smooth'});
711
+ }} initialModel={selectedModel} onModelSaved={(model) => {
712
+ setRefreshTime(new Date().getTime());
713
+ handleModelSelect(model);
714
+ }}/>)}
715
+
716
+ <div className="hidden-anchor" ref={mainPartRef}></div>
717
+
718
+ {showDataEditor && (<DataEditor
719
+ key={selectedModel?.name}
720
+ isLoading={isLoading}
721
+ model={selectedModel}
722
+ formData={formData}
723
+ setFormData={setFormData}
724
+ refreshTime={refreshTime}
725
+ onSubmit={handleFormSubmit}
726
+ setRecord={setRecordToEdit}
727
+ record={recordToEdit} // Pass record to edit to DataEditor
728
+ />)}
729
+
730
+
731
+ {selectedModel && showAPIInfo && <APIInfo/>}
732
+ {selectedModel && !showAPIInfo && !generatedModels.some(g => g.name === selectedModel?.name) && (<div className="datas">
733
+
734
+ <h2 className={"field-bg p-2"}>{t(`model_${selectedModel?.name}`, selectedModel?.name)} <>({countByModel?.[selectedModel?.name]})</></h2>
735
+
736
+
737
+ {renderCurrentView()}
738
+
739
+ {isDataLoaded && currentView === 'table' && (<>
740
+ {selectedModel && (<Pagination showElementsPerPage={true} onChange={page => {
741
+ setPage(page);
742
+ setCheckedItems([]);
743
+ gtag("event", "select_content", {
744
+ content_type: "change_page",
745
+ content_id: page
746
+ });
747
+ queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page, pagedFilters[selectedModel.name], pagedSort[selectedModel.name]]);
748
+ }} page={page} setPage={setPage} totalCount={countByModel[selectedModel.name]}
749
+ hasPreviousNext={true} visibleItemsCount={5}
750
+ elementsPerPage={elementsPerPage}/>)}
751
+ <div className="actions flex">
752
+ <Button onClick={() => {
753
+ setCheckedItems(paginatedDataByModel[selectedModel.name]);
754
+ }}><Trans i18nKey={"datatable.selectAll"}>Tout sélectionner</Trans></Button>
755
+ <Button onClick={handleDeletion} disabled={!checkedItems?.length}><Trans
756
+ i18nKey={"datatable.deleteSelection"}>Supprimer la sélection</Trans></Button>
757
+ </div>
758
+ </>)}
759
+ </div>)}
760
+ </div>
761
+
762
+ <div className={"fabs"}>
763
+ <Button data-tooltip-id={"tooltipField"} data-tooltip-html={t("btns.addData", "Ajouter une donnée au modèle")} className="fab plus-fab" onClick={() => {
764
+ mainPartRef.current.scrollIntoView({behavior: "smooth"});
765
+ handleAddData(selectedModel);
766
+ }}><FaPlus/></Button>
767
+ {me && <AssistantChat config={assistantConfig} />}
768
+ <NotificationList />
769
+ </div>
770
+
771
+ <DialogProvider>
772
+ {importModalVisible && (<Dialog onClose={() => {
773
+ setImportModalVisible(false)
774
+ }} isModal={true} isClosable={true}>
775
+ <ModelImporter onImport={onImportModels}/>
776
+ </Dialog>)}
777
+
778
+ {showPackGallery && (
779
+ <Dialog isClosable={true} isModal={true} onClose={() => setShowPackGallery(false)}>
780
+ <PackGallery />
781
+ </Dialog>
782
+ )}
783
+
784
+ <CalendarConfigModal
785
+ isOpen={isCalendarModalOpen}
786
+ onClose={() => setCalendarModalOpen(false)}
787
+ onSave={handleSaveCalendarConfig}
788
+ modelFields={selectedModel?.fields ||[]}
789
+ initialSettings={currentModelViewSettings.calendar}
790
+ />
791
+
792
+ <KanbanConfigModal
793
+ isOpen={isKanbanModalOpen}
794
+ onClose={() => setKanbanModalOpen(false)}
795
+ onSave={handleSaveKanbanConfig}
796
+ model={selectedModel}
797
+ modelFields={selectedModel?.fields||[]}
798
+ initialSettings={currentModelViewSettings.kanban}
799
+ />
800
+ </DialogProvider>
801
+ </>
802
+ );
803
+ }
804
+
805
+
756
806
  export default DataLayout;