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.
- package/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- 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
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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 (
|
|
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
|
-
|
|
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
|
|
669
|
+
<div
|
|
670
|
+
className="flex flex-no-wrap mg-item">
|
|
630
671
|
{hint('modelcreator.min.hint')}
|
|
631
|
-
<
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
652
|
-
|
|
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>
|
package/client/src/ModelList.jsx
CHANGED
|
@@ -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
|
|
55
|
+
const allTags = useMemo(() => {
|
|
41
56
|
if (!models) return [];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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={'
|
|
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
|
-
|
|
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
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|