data-primals-engine 1.4.3 → 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 (77) hide show
  1. package/README.md +913 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/AddWidgetTypeModal.jsx +47 -43
  5. package/client/src/App.jsx +3 -7
  6. package/client/src/App.scss +25 -3
  7. package/client/src/AssistantChat.jsx +363 -323
  8. package/client/src/AssistantChat.scss +30 -12
  9. package/client/src/Dashboard.jsx +480 -396
  10. package/client/src/Dashboard.scss +1 -1
  11. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  12. package/client/src/DashboardView.jsx +104 -19
  13. package/client/src/DataEditor.jsx +12 -5
  14. package/client/src/DataLayout.jsx +805 -762
  15. package/client/src/DataLayout.scss +14 -0
  16. package/client/src/DataTable.jsx +63 -77
  17. package/client/src/Dialog.scss +1 -1
  18. package/client/src/Field.jsx +591 -322
  19. package/client/src/FlexDataRenderer.jsx +2 -0
  20. package/client/src/FlexTreeUtils.js +1 -1
  21. package/client/src/FlexViewCard.jsx +44 -0
  22. package/client/src/HistoryDialog.jsx +47 -14
  23. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  24. package/client/src/HtmlViewBuilderModal.scss +18 -0
  25. package/client/src/HtmlViewCard.jsx +44 -0
  26. package/client/src/HtmlViewCard.scss +35 -0
  27. package/client/src/KPIDialog.jsx +11 -1
  28. package/client/src/KanbanCard.jsx +1 -2
  29. package/client/src/ModelCreator.jsx +6 -6
  30. package/client/src/ModelCreatorField.jsx +74 -31
  31. package/client/src/ModelList.jsx +93 -54
  32. package/client/src/Notification.jsx +136 -136
  33. package/client/src/Notification.scss +0 -18
  34. package/client/src/Pagination.jsx +5 -3
  35. package/client/src/RelationField.jsx +354 -258
  36. package/client/src/RelationSelectorWidget.jsx +173 -0
  37. package/client/src/constants.js +1 -1
  38. package/client/src/contexts/ModelContext.jsx +10 -1
  39. package/client/src/contexts/UIContext.jsx +72 -63
  40. package/client/src/filter.js +262 -212
  41. package/client/src/hooks/useTutorials.jsx +62 -65
  42. package/client/src/hooks/useValidation.js +75 -0
  43. package/client/src/translations.js +26 -24
  44. package/package.json +3 -1
  45. package/perf/README.md +147 -0
  46. package/perf/artillery-hooks.js +37 -0
  47. package/perf/perf-shot-hardwork.yml +84 -0
  48. package/perf/perf-shot-search.yml +45 -0
  49. package/perf/setup.yml +26 -0
  50. package/server.js +1 -1
  51. package/src/constants.js +3 -28
  52. package/src/core.js +15 -1
  53. package/src/data.js +1 -1
  54. package/src/defaultModels.js +63 -7
  55. package/src/email.js +5 -2
  56. package/src/engine.js +5 -3
  57. package/src/filter.js +5 -3
  58. package/src/i18n.js +710 -10
  59. package/src/modules/assistant/assistant.js +151 -19
  60. package/src/modules/bucket.js +14 -16
  61. package/src/modules/data/data.backup.js +11 -8
  62. package/src/modules/data/data.core.js +118 -92
  63. package/src/modules/data/data.history.js +531 -492
  64. package/src/modules/data/data.js +9 -56
  65. package/src/modules/data/data.operations.js +3282 -2999
  66. package/src/modules/data/data.relations.js +686 -686
  67. package/src/modules/data/data.routes.js +118 -24
  68. package/src/modules/data/data.scheduling.js +2 -1
  69. package/src/modules/data/data.validation.js +85 -3
  70. package/src/modules/file.js +247 -236
  71. package/src/modules/user.js +4 -1
  72. package/src/modules/workflow.js +9 -10
  73. package/src/openai.jobs.js +2 -0
  74. package/src/packs.js +5482 -5461
  75. package/src/providers.js +22 -7
  76. package/test/data.integration.test.js +136 -2
  77. package/test/import_export.integration.test.js +1 -1
@@ -1,4 +1,4 @@
1
- import React, {useState} from "react";
1
+ import React, {useEffect, useState} from "react";
2
2
  import {useModelContext} from "./contexts/ModelContext.jsx";
3
3
  import {useAuthContext} from "./contexts/AuthContext.jsx";
4
4
  import {isLocalUser} from "../../src/data.js";
@@ -43,6 +43,13 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
43
43
  const [showMore, setMoreVisible] = useState(false);
44
44
  const hint = (description) => t(description, '') && <div className="hint-icon"><FaCircleInfo data-tooltip-id={`tooltipHint`} data-tooltip-content={description} /></div>
45
45
 
46
+ useEffect(() => {
47
+ // S'assure que replacement est défini si un masque existe, avec une valeur par défaut correcte.
48
+ // La chaîne "\\d" en JS devient "\d" dans l'objet, ce qui est correct pour new RegExp().
49
+ if( field.mask && !field.replacement )
50
+ field.replacement = { "_": "\\d" };
51
+ }, [field]);
52
+
46
53
  return (
47
54
  <div className="field-edit">
48
55
 
@@ -72,7 +79,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
72
79
  newFields[index].name = e.target.value;
73
80
  setFields(newFields);
74
81
  }}
75
- help={t('modelcreator.name.hint')}
76
82
  required
77
83
  after={!(!modelLocked && isLocalUser(me) && field.locked) && !field._isNewField && (
78
84
  <Button type={"button"} className={"btn-form btn-last"}
@@ -212,10 +218,10 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
212
218
 
213
219
  <div className="flex flex-no-wrap">
214
220
  {hint('modelcreator.relationFilter.hint')}
215
- <label className="checkbox-label flex flex-1"><Trans
216
- i18nKey={"modelcreator.relationFilter"}>Filtre</Trans> :
217
- <input
218
- type="checkbox"
221
+ <div className="checkbox-label flex flex-1">
222
+ <CheckboxField
223
+ label={<Trans
224
+ i18nKey={"modelcreator.relationFilter"}>Filtre</Trans>}
219
225
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
220
226
  checked={field.relationFilter !== undefined}
221
227
  onChange={(e) => {
@@ -228,7 +234,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
228
234
  setFields(newFields);
229
235
  }}
230
236
  />
231
- </label>
237
+ </div>
232
238
  </div>)}
233
239
 
234
240
 
@@ -286,37 +292,35 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
286
292
  newFields[index].multiline = e;
287
293
  setFields(newFields);
288
294
  }}
289
- help={field.multiline && t('modelcreator.multiline.hint')}
290
295
  />
291
296
  </div>
292
297
  </div>
298
+
293
299
  </>
294
300
  )}
295
301
 
296
302
  {['string', 'string_t', 'richtext', 'richtext_t', 'email', 'phone', 'url', 'password', 'code'].includes(field.itemsType || field.type) && (
297
303
  <>
298
- <div className={"flex flex-no-wrap"}>
304
+ <div className={"flex flex-no-wrap field-bg"}>
299
305
  {hint('modelcreator.maxlength.hint')}
300
- <label className={"flex flex-1"}>
301
- <Trans i18nKey={"modelcreator.maxlength"}>Longueur
302
- maximale :</Trans>
303
- <NumberField
306
+ <div className={"flex-1"}><NumberField
304
307
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
305
308
  step={1}
306
309
  min={0}
310
+ label={<Trans i18nKey={"modelcreator.maxlength"}>Longueur
311
+ maximale :</Trans>}
307
312
  className={"flex-1"}
308
313
  value={field.maxlength}
309
314
  onChange={(e) => {
310
315
  const newFields = [...fields];
311
- const val = parseInt(e, 10);
312
- if (!val)
316
+ const val = parseInt(e.target.value, 10);
317
+ if (isNaN(val))
313
318
  newFields[index].maxlength = undefined;
314
319
  else
315
320
  newFields[index].maxlength = val;
316
321
  setFields(newFields);
317
322
  }}
318
- />
319
- </label>
323
+ /></div>
320
324
  </div>
321
325
  </>
322
326
  )}
@@ -438,7 +442,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
438
442
  newFields[index].required = e;
439
443
  setFields(newFields);
440
444
  }}
441
- help={field.required && t('modelcreator.required.hint')}
442
445
  />
443
446
  </div>
444
447
  </div>
@@ -456,7 +459,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
456
459
  newFields[index].unique = e;
457
460
  setFields(newFields);
458
461
  }}
459
- help={field.unique && t('modelcreator.unique.hint')}
460
462
  />
461
463
  </div>
462
464
  </div>
@@ -555,7 +557,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
555
557
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
556
558
  onChange={(e) => {
557
559
  const newFields = [...fields];
558
-
559
560
  newFields[index].default = e.value;
560
561
  setFields(newFields);
561
562
  }}
@@ -584,8 +585,47 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
584
585
  </>)}
585
586
  </div>)}
586
587
 
588
+ {["string_t", "string"].includes(field.itemsType || field.type) && (
589
+ <>
587
590
 
588
- {field.type === 'number' && (
591
+ <div className={"flex flex-no-wrap field-bg"}>
592
+ {hint('modelcreator.mask.hint')}
593
+ <div className={"flex-1"}>
594
+ <TextField
595
+ disabled={modelLocked || (isLocalUser(me) && field.locked)}
596
+ label={<Trans i18nKey={"modelcreator.mask"}>Masque de saisie</Trans>}
597
+ className={"flex-1"}
598
+ value={field.mask}
599
+ placeholder={t('modelcreator.field.mask.ph', "mask_")}
600
+ onChange={(e) => {
601
+ const newFields = [...fields];
602
+ newFields[index].mask = e.target.value;
603
+ setFields(newFields);
604
+ }}
605
+ />
606
+ </div>
607
+ </div>
608
+ <div className={"flex flex-no-wrap field-bg"}>
609
+ {hint('modelcreator.replacement.hint')}
610
+ <div className={"flex-1"}>
611
+ <CodeField
612
+ name="replacement"
613
+ disabled={modelLocked || (isLocalUser(me) && field.locked)}
614
+ label={<Trans i18nKey={"modelcreator.replacement"}>Règles de remplacement (JSON)</Trans>}
615
+ language="json"
616
+ value={field.replacement ? JSON.stringify(field.replacement, null, 2) : ''}
617
+ onChange={(e) => {
618
+ const newFields = [...fields];
619
+ newFields[index].replacement = e.value;
620
+ setFields(newFields);
621
+ }}
622
+ />
623
+ </div>
624
+ </div>
625
+ </>
626
+ )}
627
+
628
+ {(field.itemsType || field.type) === 'number' && (
589
629
  <>
590
630
  <div
591
631
  className="flex flex-no-wrap mg-item">{hint('modelcreator.min.hint')}
@@ -626,10 +666,12 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
626
666
 
627
667
  {(['datetime', 'date'].includes(field.itemsType || field.type)) && (
628
668
  <>
629
- <div className="flex flex-no-wrap mg-item">
669
+ <div
670
+ className="flex flex-no-wrap mg-item">
630
671
  {hint('modelcreator.min.hint')}
631
- <label className={"flex-1"}><Trans i18nKey={"modelcreator.min"}>Valeur minimale :</Trans></label>
632
- <div className="flex flex-1 field-bg "><input
672
+ <div className="flex field-bg flex-1">
673
+ <label className={"flex-1"}><Trans i18nKey={"modelcreator.min"}>Valeur minimale :</Trans></label>
674
+ <input
633
675
  className={"flex-1"}
634
676
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
635
677
  type={field.type === 'datetime' ? 'datetime-local' : field.type}
@@ -643,13 +685,16 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
643
685
  }
644
686
  setFields(newFields);
645
687
  }}
646
- /></div>
688
+ />
689
+ </div>
647
690
  </div>
648
691
 
649
692
  <div className="flex flex-no-wrap mg-item">
650
693
  {hint('modelcreator.max.hint')}
651
- <label className={"flex-1"}><Trans i18nKey={"modelcreator.max"}>Valeur maximale :</Trans></label>
652
- <div className="flex flex-1 field-bg "><input
694
+ <div className="flex field-bg flex-1">
695
+ <label className={"flex-1"}><Trans i18nKey={"modelcreator.max"}>Valeur maximale :</Trans></label>
696
+ <input
697
+ className={"flex-1"}
653
698
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
654
699
  type={field.type === 'datetime' ? 'datetime-local' : field.type}
655
700
  value={field.type === "number" ? (field.max + '').replace('.', ',') : field.max}
@@ -687,7 +732,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
687
732
  </div>
688
733
 
689
734
 
690
- {field.type=== 'number'&&(
735
+ {(field.itemsType || field.type)=== 'number'&&(
691
736
  <div className="flex flex-no-wrap">
692
737
  {hint('modelcreator.gauge.hint')}
693
738
  <div className="checkbox-label flex flex-1">
@@ -738,7 +783,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
738
783
 
739
784
  </div>
740
785
  )}
741
- {field.type === 'code' && (<>
786
+ {(field.itemsType || field.type) === 'code' && (<>
742
787
  <div className="flex">
743
788
  {hint('modelcreator.language.hint')}
744
789
  <label className="checkbox-label flex flex-1">
@@ -807,7 +852,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
807
852
  newFields[index].asMain = e;
808
853
  setFields(newFields);
809
854
  }}
810
- help={field.asMain && t('modelcreator.asMain.hint')}
811
855
  />
812
856
  </div>
813
857
  </div>)}
@@ -858,7 +902,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
858
902
  newFields[index].hiddenable = e;
859
903
  setFields(newFields);
860
904
  }}
861
- help={field.unique && t('modelcreator.hiddenable.hint')}
862
905
  />
863
906
  </div>
864
907
  </div>
@@ -3,7 +3,7 @@ import {Trans, useTranslation} from "react-i18next";
3
3
  import {useAuthContext} from "./contexts/AuthContext.jsx";
4
4
  import {useMutation, useQueryClient} from "react-query";
5
5
  import React, {useEffect, useMemo, useState} from "react";
6
- import {FaBook, FaEdit, FaFileImport, FaPlus} from "react-icons/fa";
6
+ import {FaBook, FaBoxOpen, FaEdit, FaFileImport, FaPlus} from "react-icons/fa";
7
7
  import Button from "./Button.jsx";
8
8
  import useLocalStorage from "./hooks/useLocalStorage.js";
9
9
  import {Tooltip} from "react-tooltip";
@@ -12,7 +12,7 @@ import {profiles} from "./constants.js";
12
12
  import * as FaIcons from "react-icons/fa";
13
13
  import * as Fa6Icons from "react-icons/fa6";
14
14
 
15
-
15
+ // --- SUGGESTION: Extraire cette logique dans un composant dédié si elle devient plus complexe ---
16
16
  // Fonction pour obtenir le composant icône par son nom
17
17
  const getIconComponent = (iconName) => {
18
18
  if (!iconName) return null;
@@ -20,8 +20,22 @@ const getIconComponent = (iconName) => {
20
20
  return IconComponent ? <IconComponent /> : null; // Retourne l'élément React ou null
21
21
  };
22
22
 
23
+ // --- SUGGESTION: Créer un petit composant pour la lisibilité de l'affichage du nom du modèle ---
24
+ const ModelListItemLabel = ({ model, count, isGenerated, t }) => {
25
+ const modelName = t(`model_${model.name}`, model.name);
26
+
27
+ let suffix = '';
28
+ if (isGenerated) {
29
+ suffix = ` (${t('models.status.tmp', 'brouillon')})`; // Utiliser i18n pour 'tmp'
30
+ } else if (count > 0) {
31
+ suffix = ` (${count})`;
32
+ }
33
+
34
+ return <>{modelName}{suffix}</>;
35
+ };
36
+
23
37
 
24
- export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditModel, onAPIInfo, onNewData }) {
38
+ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditModel, onAPIInfo, onNewData, onImportPack }) {
25
39
  const {allTourSteps, setIsTourOpen,setCurrentTourSteps, setTourStepIndex, currentTour, setCurrentTour} = useUI();
26
40
 
27
41
  const {models, setSelectedModel, selectedModel, countByModel, generatedModels} = useModelContext();
@@ -29,6 +43,7 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
29
43
  const {me} = useAuthContext();
30
44
  const queryClient = useQueryClient()
31
45
  const [searchTerm, setSearchTerm] = useState('');
46
+ const [selectedTag, setSelectedTag] = useLocalStorage('modelList-selectedTag', 'all');
32
47
 
33
48
  const [currentProfile, setCurrentProfile] = useLocalStorage('profile', null);
34
49
  useEffect(() =>{
@@ -37,20 +52,38 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
37
52
  }
38
53
  }, [me])
39
54
 
40
- const filteredModels = useMemo(() => {
55
+ const allTags = useMemo(() => {
41
56
  if (!models) return [];
42
- if (!searchTerm.trim()) return models; // Si la recherche est vide, retourne tous les modèles
43
-
44
- const lowerSearchTerm = searchTerm.toLowerCase();
45
- return models.filter(model => {
46
- return (model.name && t('model_' + model.name, model.name).toLowerCase().includes(lowerSearchTerm)) ||
47
- (model.fields.some(f =>
48
- (model.icon && model.icon.toLowerCase().includes(lowerSearchTerm)) ||
49
- f.name?.toLowerCase().includes(lowerSearchTerm) ||
50
- f.hint?.toLowerCase().includes(lowerSearchTerm)) ||
51
- (model.description && model.description.toLowerCase().includes(lowerSearchTerm)))
57
+ const tagsSet = new Set();
58
+ models
59
+ .filter(model => model._user === me?.username) // On ne prend que les tags des modèles de l'utilisateur
60
+ .forEach(model => {
61
+ if (model.tags && Array.isArray(model.tags)) {
62
+ model.tags.forEach(tag => tagsSet.add(tag));
63
+ }
52
64
  });
53
- }, [models, searchTerm]);
65
+ return Array.from(tagsSet).sort();
66
+ }, [models, me?.username]);
67
+
68
+ console.log(allTags,"t");
69
+ const filteredModels = useMemo(() => {
70
+ if (!models) return [];
71
+ let results = models.filter(model => model._user === me?.username);
72
+
73
+ // Filtrage par tag
74
+ if (selectedTag && selectedTag !== 'all') {
75
+ results = results.filter(model => model.tags?.includes(selectedTag));
76
+ }
77
+
78
+ // Filtrage par terme de recherche
79
+ if (searchTerm.trim()) {
80
+ const lowerSearchTerm = searchTerm.toLowerCase();
81
+ results = results.filter(model =>
82
+ (t('model_' + model.name, model.name).toLowerCase().includes(lowerSearchTerm)) ||
83
+ (model.description && model.description.toLowerCase().includes(lowerSearchTerm)));
84
+ }
85
+ return results;
86
+ }, [models, searchTerm, selectedTag, me?.username, t]);
54
87
 
55
88
  const handleSelectModel = (model) => {
56
89
  setSelectedModel(model);
@@ -96,28 +129,6 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
96
129
  });
97
130
  };
98
131
 
99
- const [mods, setMods] = useState([]);
100
- useEffect(() => {
101
- if( countByModel && selectedModel)
102
- {
103
- setMods((m) => {
104
- const counts = [...m];
105
- const mod = counts.find(f => f.mod === selectedModel.name);
106
- if( mod ){
107
- mod.count = countByModel[selectedModel.name];
108
- return counts.map((c => {
109
- if( c.mod === mod.mod) {
110
- return {...mod};
111
- }
112
- return c;
113
- }));
114
- }else{
115
- return [...m, { mod: selectedModel.name, count: countByModel[selectedModel.name]}];
116
- }
117
- })
118
- }
119
- }, [countByModel, selectedModel]);
120
-
121
132
  // --- NEW: Effect to update tour steps when a profile is already selected on mount ---
122
133
  useEffect(() => {
123
134
  if (currentProfile && currentTour && /^demo[0-9]{1,2}$/.test(me?.username)) { // Only run on demo user for simplicity
@@ -163,9 +174,9 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
163
174
  }} />
164
175
  </div></div>}
165
176
  <div className="models">
166
- <h2><Trans i18nKey="models">Modèles</Trans></h2>
177
+ <h2 className={"field-bg p-2"}><Trans i18nKey="models">Modèles</Trans></h2>
167
178
  <div className="model-list-container">
168
- <div className="model-list-search-bar-container">
179
+ <div className="flex flex-no-wrap model-list-search-bar-container">
169
180
  <input
170
181
  type="search"
171
182
  placeholder={t('models.searchPlaceholder', 'Rechercher un modèle...')}
@@ -173,6 +184,18 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
173
184
  onChange={(e) => setSearchTerm(e.target.value)}
174
185
  className="model-list-search-input"
175
186
  />
187
+ {allTags.length > 0 && (
188
+ <div className="model-list-tag-filter">
189
+ <select value={selectedTag} onChange={(e) => setSelectedTag(e.target.value)}>
190
+ <option value="all">{t('models.tags.all', 'Tous les tags')}</option>
191
+ {allTags.map(tag => (
192
+ <option key={tag} value={tag}>
193
+ {t(`tags.${tag}`, tag.charAt(0).toUpperCase() + tag.slice(1))}
194
+ </option>
195
+ ))}
196
+ </select>
197
+ </div>
198
+ )}
176
199
  </div>
177
200
  {filteredModels.length > 0 ? (
178
201
  <div className="model-list">
@@ -185,15 +208,23 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
185
208
 
186
209
  const IconComponent = getIconComponent(model.icon);
187
210
 
211
+ // --- SUGGESTION: Rendre le comportement du clic prévisible ---
212
+ // Le clic principal sélectionne toujours le modèle. L'édition est une action secondaire via son bouton.
213
+ const handleItemClick = () => {
214
+ onModelSelect(model);
215
+ };
216
+
188
217
  return (
189
218
  <li data-testid={'model_'+model.name} className={`${model.name === selectedModel?.name ? 'active' : ''}`}
190
- key={'modelist' + model.name} onClick={() => generatedModels.some(g => g.name === model.name) ? onEditModel(model) : onModelSelect(model)}>
219
+ key={'modellist' + model.name} onClick={handleItemClick}>
191
220
  <div className="flex flex-center flex-fw">
192
221
  <div
193
- className="flex flex-1 flex-no-wrap break-word">
222
+ className="flex flex-1 flex-no-wrap break-word gap-2">
194
223
  <div className={"icon"}>{IconComponent ? IconComponent : <></>}</div>
195
- <div>{t(`model_${model.name}`, model.name)} {generatedModels.some(f => f.name === model.name) ? '(tmp)' : (mods.some(f => f.mod === model.name) ? `(${mods.find(f => f.mod === model.name).count})` : '')}</div></div>
224
+ {/* --- SUGGESTION: Utiliser le composant pour la clarté --- */}
225
+ <div><ModelListItemLabel model={model} count={countByModel?.[model.name]} isGenerated={generatedModels.some(f => f.name === model.name)} t={t} /></div></div>
196
226
  <div className="btns">
227
+ {/* Le bouton "Ajouter" est conditionnel, ce qui est bien */}
197
228
  {!generatedModels.some(g => g.name === model.name) && (<button data-tooltip-id="tooltipML" data-tooltip-content={t('btns.addData')} onClick={(e) => {
198
229
  e.stopPropagation();
199
230
  e.preventDefault();
@@ -216,21 +247,29 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
216
247
  </li>)
217
248
  })}
218
249
  </ul>
219
-
220
250
  </div>) : (
221
- <p className="no-models-found">
222
- {searchTerm ? t('models.noMatch', 'Aucun modèle ne correspond à votre recherche.') : t('models.noModels', 'Aucun modèle disponible.')}
223
- </p>
251
+ <div className="empty-state-container">
252
+ <div className="empty-state-content">
253
+ <div className="empty-state-icon">
254
+ {/* Une icône SVG simple pour représenter des "blocs de construction" ou des "données" */}
255
+ <svg width="80" height="80" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
256
+ <path d="M14 10L14 4L20 4L20 10L14 10Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
257
+ <path d="M4 20L4 14L10 14L10 20L4 20Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
258
+ <path d="M4 10L4 4L10 4L10 10L4 10Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
259
+ <path d="M14 20L14 14L20 14L20 20L14 20Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
260
+ </svg>
261
+ </div>
262
+ <h3>{t('models.empty.title', 'Commencez à structurer vos données')}</h3>
263
+ <p>
264
+ {searchTerm ?
265
+ t('models.noMatch', 'Aucun modèle ne correspond à votre recherche.') :
266
+ t('models.empty.description', 'Créez votre premier modèle de A à Z ou importez une structure existante pour démarrer rapidement.')
267
+ }
268
+ </p>
269
+ </div>
270
+ </div>
224
271
  )}
225
- <div className="flex actions">
226
- <Button onClick={onCreateModel} className="btn tourStep-create-model"><FaPlus/><Trans
227
- i18nKey="btns.createModel">Créer un modèle</Trans></Button>
228
- <Button onClick={onImportModel}
229
- className="btn tourStep-import-model btn-primary"><FaFileImport/><Trans
230
- i18nKey="btns.importModels">Importer un modèle</Trans></Button>
231
- </div>
232
272
  </div>
233
273
  </div>
234
274
  </>
235
275
  }
236
-