data-primals-engine 1.2.3 → 1.2.5

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 (58) hide show
  1. package/CONTRIBUTING.md +91 -0
  2. package/README.md +50 -22
  3. package/client/src/App.jsx +0 -5
  4. package/client/src/App.scss +6 -0
  5. package/client/src/ConditionBuilder.scss +34 -1
  6. package/client/src/ConditionBuilder2.jsx +179 -53
  7. package/client/src/ContentView.jsx +0 -3
  8. package/client/src/CronBuilder.jsx +0 -1
  9. package/client/src/CronPartBuilder.jsx +0 -2
  10. package/client/src/DashboardView.jsx +0 -5
  11. package/client/src/DataEditor.jsx +8 -10
  12. package/client/src/DataImporter.jsx +469 -0
  13. package/client/src/DataLayout.jsx +0 -1
  14. package/client/src/DataTable.jsx +2 -368
  15. package/client/src/DataTable.scss +18 -1
  16. package/client/src/Field.jsx +85 -48
  17. package/client/src/FlexBuilder.jsx +1 -1
  18. package/client/src/ModelCreator.jsx +29 -25
  19. package/client/src/ModelCreator.scss +13 -0
  20. package/client/src/ModelCreatorField.jsx +1 -5
  21. package/client/src/RTE.jsx +1 -6
  22. package/client/src/RTETrans.jsx +0 -2
  23. package/client/src/RelationField.jsx +1 -1
  24. package/client/src/RelationValue.jsx +1 -2
  25. package/client/src/TourSpotlight.jsx +0 -2
  26. package/client/src/filter.js +87 -0
  27. package/client/src/hooks/data.js +1 -3
  28. package/client/src/hooks/useTutorials.jsx +0 -1
  29. package/client/src/translations.js +60 -26
  30. package/package.json +4 -3
  31. package/server.js +2 -2
  32. package/src/data.js +8 -0
  33. package/src/email.js +2 -2
  34. package/src/engine.js +59 -20
  35. package/src/index.js +1 -1
  36. package/src/middlewares/middleware-mongodb.js +0 -1
  37. package/src/modules/assistant.js +1 -3
  38. package/src/modules/bucket.js +3 -4
  39. package/src/modules/data/data.core.js +17 -0
  40. package/src/modules/{data.js → data/data.js} +4595 -5991
  41. package/src/modules/data/data.routes.js +1637 -0
  42. package/src/modules/data/index.js +1 -0
  43. package/src/modules/file.js +1 -1
  44. package/src/modules/mongodb.js +0 -1
  45. package/src/modules/user.js +1 -1
  46. package/src/modules/workflow.js +38 -38
  47. package/src/packs.js +4 -1
  48. package/test/data.backup.integration.test.js +4 -5
  49. package/test/data.integration.test.js +2 -6
  50. package/test/events.test.js +1 -1
  51. package/test/file.test.js +1 -4
  52. package/test/import_export.integration.test.js +22 -15
  53. package/test/model.integration.test.js +8 -10
  54. package/test/user.test.js +2 -2
  55. package/test/vm.test.js +1 -1
  56. package/test/workflow.integration.test.js +17 -14
  57. package/test/workflow.robustness.test.js +15 -10
  58. package/src/modules/test +0 -147
@@ -57,10 +57,8 @@ import PackGallery from "./PackGallery.jsx";
57
57
  import {HiddenableCell} from "./HiddenableCell.jsx";
58
58
  import ConditionBuilder from "./ConditionBuilder.jsx";
59
59
  import {pagedFilterToMongoConds} from "./filter.js";
60
- import {isConditionMet} from "data-primals-engine/filter";
61
-
62
- // Ajoutez cette constante pour la clé de sessionStorage
63
- const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
60
+ import {isConditionMet} from "../../src/filter";
61
+ import {DataImporter} from "./DataImporter.jsx";
64
62
 
65
63
  const Header = ({
66
64
  reversed = false,
@@ -685,10 +683,8 @@ export function DataTable({
685
683
  />
686
684
  <ExportDialog isOpen={showExportDialog} onClose={() => {
687
685
  setExportDialogVisible(false);
688
- console.log("close")
689
686
  }} availableModels={models} currentModel={selectedModel.name} hasSelection={true} onExport={(data)=>{
690
687
  exportMutation(data);
691
- console.log(data);
692
688
  }} />
693
689
  {showPackGallery && (
694
690
  <Dialog isClosable={true} isModal={true} onClose={() => setShowPackGallery(false)}>
@@ -701,365 +697,3 @@ export function DataTable({
701
697
  );
702
698
  }
703
699
 
704
-
705
- function DataImporter({onClose}) {
706
-
707
- const [file, setFile] = useState(null);
708
- const {selectedModel, page} = useModelContext();
709
-
710
- const {me} = useAuthContext();
711
- const {t, i18n} = useTranslation();
712
-
713
- const queryClient = useQueryClient();
714
- const {addNotification} = useNotificationContext();
715
-
716
- const [hasHeaders, setHasHeaders] = useState(true);
717
- const [csvHeaders, setCSVHeaders] = useState(selectedModel.fields.map(field => field.name));
718
-
719
- // --- MODIFIÉ : État pour gérer plusieurs tâches d'importation ---
720
- // Cet objet stockera les données de progression de chaque tâche, indexées par leur jobId
721
- const [importJobs, setImportJobs] = useState({});
722
- // Cette liste stockera les IDs des tâches qui sont actuellement suivies via SSE
723
- const [activeJobIds, setActiveJobIds] = useState([]);
724
-
725
- // Référence pour stocker les instances EventSource, indexées par jobId
726
- const eventSourceRefs = useRef({});
727
-
728
- const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
729
-
730
- const [storedJobIds, setStoredJobIds] = useLocalStorage(SESSION_STORAGE_IMPORT_JOBS_KEY, []);
731
- // --- NOUVEAU : Charger/Sauvegarder les IDs des tâches actives depuis sessionStorage ---
732
- useEffect(() => {
733
- // Charger les IDs des tâches actives depuis sessionStorage au montage
734
- setActiveJobIds(storedJobIds);
735
-
736
- // Pour chaque jobId stockpour obtenir le dernier statut
737
- storedJobIds.forEach(jobId => {
738
- startProgressTracking(jobId);
739
- });
740
-
741
- // Fonction de nettoyage : fermer toutes les connexions EventSource lors du démontage du composant
742
- return () => {
743
- Object.values(eventSourceRefs.current).forEach(es => es.close());
744
- eventSourceRefs.current = {}; // Effacer les références
745
- };
746
- }, []); // S'exécute une seule fois au montage
747
-
748
- // --- NOUVEAU : Effet pour mettre à jour sessionStorage lorsque activeJobIds change ---
749
- useEffect(() => {
750
- setStoredJobIds(activeJobIds);
751
- }, [activeJobIds]);
752
-
753
-
754
- // Mutation pour initier l'importation (envoi du fichier au serveur)
755
- const {isLoading, mutate: importMutation} = useMutation(async () => {
756
- console.log('Initiating data import...');
757
- const params = new FormData();
758
- params.append('model', selectedModel?.name);
759
- params.append("_user", getUserId(me));
760
- params.append("hasHeaders", !!hasHeaders);
761
- params.append("csvHeaders", csvHeaders.join(','));
762
- if (file) {
763
- params.append("file", file);
764
- } else {
765
- addNotification({ title: t('dataimporter.noFileSelected', 'Veuillez sélectionner un fichier à importer.'), status: 'warning' });
766
- return Promise.reject(new Error("No file selected"));
767
- }
768
-
769
- try {
770
- const response = await fetch(`/api/data/import?lang=${lang}`, {
771
- method: 'POST',
772
- body: params
773
- });
774
-
775
- if (response.status === 202) {
776
- const { job } = await response.json();
777
- const { jobId} = job;
778
-
779
- // --- MODIFIÉ : Ajouter le nouvel jobId à activeJobIds et à l'état importJobs ---
780
- setActiveJobIds(prevIds => [...prevIds, jobId]);
781
- setImportJobs(prevJobs => ({
782
- ...prevJobs,
783
- [jobId]: {
784
- jobId,
785
- status: 'pending',
786
- totalRecords: 0,
787
- processedRecords: 0,
788
- errors: [],
789
- // Ajoutez d'autres champs initiaux que vous souhaitez afficher immédiatement
790
- }
791
- }));
792
- startProgressTracking(jobId); // Commencer le suivi de cette nouvelle tâche
793
- addNotification({
794
- title: t('dataimporter.initiated', 'Importation initiée. Suivi de la progression...'),
795
- icon: <FaInfo/>,
796
- status: 'info'
797
- });
798
- } else {
799
- const errorData = await response.json();
800
- addNotification({
801
- title: errorData.error || t('dataimporter.error', 'Erreur lors de l\'importation.'),
802
- status: 'error'
803
- });
804
- }
805
- } catch (e) {
806
- addNotification({
807
- title: e.message || t('dataimporter.networkError', 'Erreur réseau lors de l\'importation.'),
808
- status: 'error'
809
- });
810
- }
811
- });
812
-
813
- // Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
814
-
815
- // Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
816
- const startProgressTracking = (jobId) => {
817
- // Fermer toute connexion EventSource existante pour ce jobId pour éviter les doublons
818
- if (eventSourceRefs.current[jobId]) {
819
- eventSourceRefs.current[jobId].close();
820
- }
821
-
822
- const eventSource = new EventSource(`/api/import/progress/${jobId}`);
823
- eventSourceRefs.current[jobId] = eventSource; // Stocker l'instance pour le nettoyage
824
-
825
- eventSource.onmessage = (event) => {
826
- const data = JSON.parse(event.data);
827
- // --- MODIFIÉ : Mettre à jour la progression de la tâche spécifique ---
828
- setImportJobs(prevJobs => ({
829
- ...prevJobs,
830
- [jobId]: data
831
- }));
832
-
833
- // Si la tâche est terminée (succès ou échec), fermer la connexion SSE.
834
- // NE PAS la retirer de activeJobIds ici, pour qu'elle persiste au rafraîchissement.
835
- // Elle sera retirar le bouton "Effacer".
836
- if (data.status === 'completed' || data.status === 'failed' || data.status === 'not_found') {
837
- eventSource.close();
838
- delete eventSourceRefs.current[jobId]; // Supprimer la référence de l'EventSource
839
-
840
- // --- LIGNE MODIFIÉE/SUPPRIMÉE ---
841
- // Supprimez ou commentez la ligne suivante :
842
- // setActiveJobIds(prevIds => prevIds.filter(id => id !== jobId));
843
-
844
- queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page]); // Rafraîchir les données du tableau
845
-
846
- if (data.status === 'completed') {
847
- addNotification({
848
- title: t('dataimporter.success', 'Importation des données réussie.'),
849
- icon: <FaInfo/>,
850
- status: 'completed'
851
- });
852
- } else if (data.status === 'failed') {
853
- addNotification({
854
- title: t('dataimporter.failed', 'Importation échouée. Voir les détails pour les erreurs.'),
855
- status: 'error'
856
- });
857
- } else if (data.status === 'not_found') {
858
- addNotification({
859
- title: t('dataimporter.jobNotFound', 'Tâche d\'importation non trouvée ou déjà terminée.'),
860
- status: 'warning'
861
- });
862
- }
863
- }
864
- };
865
-
866
- eventSource.onerror = (error) => {
867
- console.error(`EventSource error for job ${jobId}:`, error);
868
- eventSource.close();
869
- delete eventSourceRefs.current[jobId];
870
- setStoredJobIds(prevIds => prevIds.filter(id => id !== jobId));
871
- };
872
- };
873
-
874
- const handleImportClick = () => {
875
- importMutation();
876
- };
877
-
878
- const handleCloseModal = () => {
879
- // Fermer toutes les connexions EventSource avant de fermer la modale
880
- Object.values(eventSourceRefs.current).forEach(es => es.close());
881
- eventSourceRefs.current = {}; // Effacer les références
882
- onClose();
883
- };
884
-
885
- // Déterminer si une importation est actuellement en cours (pour désactiver les boutons)
886
- const isAnyImportInProgress = Object.values(importJobs).some(job => job.status === 'pending' || job.status === 'processing');
887
-
888
- // Filtrer et trier les tâches à afficher (par exemple, les tâches en cours en premier)
889
- const jobsToDisplay = Object.values(importJobs).sort((a, b) => {
890
- // Trier par statut (en attente/en cours en premier, puis échoué, puis terminé)
891
- const statusOrder = { 'pending': 1, 'processing': 2, 'failed': 3, 'completed': 4, 'not_found': 5 };
892
- return statusOrder[a.status] - statusOrder[b.status];
893
- });
894
-
895
- return (
896
- <Dialog isClosable={true} isModal={true} onClose={handleCloseModal}>
897
- <>
898
- <h2>
899
- <Trans i18nKey="dataimporter.title" values={{model: t('model_' + selectedModel?.name, selectedModel?.name)}}>
900
- Importer des données dans {t('model_' + selectedModel?.name, selectedModel?.name)}
901
- </Trans>
902
- </h2>
903
- <p>
904
- <Trans i18nKey="dataimporter.info" values={{constante: (maxBytesPerSecondThrottleData / kilobytes) + 'ko/s'}}></Trans>
905
- </p>
906
-
907
- {/* Toujours afficher le formulaire de sélection de fichier et le bouton d'importation */}
908
- <FileField
909
- name="file"
910
- maxSize={maxFileSize}
911
- mimeTypes={['application/json', 'text/csv']}
912
- type="file"
913
- multiple={false}
914
- onChange={(files) => {
915
- setFile(files && files.length > 0 ? files[files.length - 1].file : null);
916
- }}
917
- />
918
- {file && (file.name.endsWith('.csv') || file.type === 'text/csv') && (
919
- <div className="checkbox-label flex flex-row">
920
- <label htmlFor="hasHeadersCheckbox">
921
- <input
922
- type="checkbox"
923
- id="hasHeadersCheckbox"
924
- checked={hasHeaders}
925
- onChange={(e) => setHasHeaders(e.target.checked)}
926
- />
927
- <Trans i18nKey="dataimporter.hasCsvHeaders"></Trans>
928
- </label>
929
- {!hasHeaders && (
930
- <table>
931
- <thead>
932
- <tr>
933
- <th><Trans i18nKey={"dataimporter.csvColumn"}>Num de colonne du CSV</Trans></th>
934
- <th><Trans i18nKey="dataimporter.field">Champ du modèle</Trans></th>
935
- </tr>
936
- </thead>
937
- <tbody>
938
- {csvHeaders.map((mappedFieldName, index) => {
939
- let fieldObjectForModelField = null;
940
- let currentFieldValueForModelField = null;
941
-
942
- if (mappedFieldName && mappedFieldName.trim() !== '') {
943
- fieldObjectForModelField = selectedModel?.fields.find(f_model => f_model.name === mappedFieldName);
944
- if (fieldObjectForModelField) {
945
- currentFieldValueForModelField = fieldObjectForModelField.name;
946
- }
947
- }
948
-
949
- return (
950
- <tr key={`${selectedModel.name}-csvmap-${index}`}>
951
- <td><Trans i18nKey="dataimporter.column" values={[index + 1]}>colonne {index + 1}</Trans></td>
952
- <td>
953
- <div className="flex">
954
- <ModelField
955
- disableable={true}
956
- showModel={false}
957
- value={selectedModel.name}
958
- fieldValue={currentFieldValueForModelField}
959
- onChange={({name: propName, value: selectedValue}) => {
960
- const newCsvHeaders = [...csvHeaders];
961
- newCsvHeaders[index] = selectedValue?.field ?? '';
962
- setCSVHeaders(newCsvHeaders);
963
- }}
964
- fields={true}
965
- model={selectedModel}
966
- field={fieldObjectForModelField}
967
- />
968
- <Button className="flex" onClick={() => {
969
- const csvH = [...csvHeaders];
970
- setCSVHeaders(csvH.filter((_, i) => i !== index));
971
- }}><FaTrash/></Button>
972
- </div>
973
- </td>
974
- </tr>
975
- );
976
- })}
977
- <tr>
978
- <td colSpan={2}>
979
- <Button onClick={() => {
980
- const csvH = [...csvHeaders];
981
- csvH.push('');
982
- setCSVHeaders(csvH)
983
- }}><Trans i18nKey="dataimporter.addColumn">Ajouter une colonne</Trans></Button>
984
- </td>
985
- </tr>
986
- </tbody>
987
- </table>
988
- )}
989
- </div>
990
- )}
991
- <div>
992
- <Button onClick={handleImportClick} disabled={isLoading || !file}>
993
- <Trans i18nKey="btns.import">Importer</Trans>
994
- </Button>
995
- </div>
996
-
997
- {/* Afficher la progression pour toutes les tâches d'importation actives/suivies */}
998
- {jobsToDisplay.length > 0 && (
999
- <div className="import-jobs-list">
1000
- <h3><Trans i18nKey="dataimporter.activeImports">Importations en cours / terminées</Trans></h3>
1001
- {jobsToDisplay.map(job => {
1002
- const progressPercentage = job.totalRecords > 0
1003
- ? (job.processedRecords / job.totalRecords) * 100
1004
- : 0;
1005
- const isJobFinished = job.status === 'completed' || job.status === 'failed' || job.status === 'not_found';
1006
-
1007
- return (
1008
- <div key={job.jobId} className="import-progress-container">
1009
- <h4><Trans i18nKey="dataimporter.jobId">Tâche ID:</Trans> {job.jobId?.substring(0, 8)}...</h4>
1010
- <p>
1011
- <Trans i18nKey="dataimporter.status">Statut:</Trans>{' '}
1012
- <strong>{t(`dataimporter.status.${job.status}`, job.status)}</strong>
1013
- </p>
1014
- {job.totalRecords > 0 && (
1015
- <p>
1016
- <Trans i18nKey="dataimporter.recordsProcessed">Enregistrements traités:</Trans>{' '}
1017
- {job.processedRecords} / {job.totalRecords}
1018
- </p>
1019
- )}
1020
- <div className="progress-bar-wrapper">
1021
- <div
1022
- className="progress-bar"
1023
- style={{ width: `${progressPercentage}%` }}
1024
- >
1025
- {progressPercentage.toFixed(0)}%
1026
- </div>
1027
- </div>
1028
-
1029
- {job.errors && job.errors.length > 0 && (
1030
- <div className="import-errors">
1031
- <h4><Trans i18nKey="dataimporter.errors">Erreurs:</Trans></h4>
1032
- <ul>
1033
- {job.errors.map((error, index) => (
1034
- <li key={index}>{error}</li>
1035
- ))}
1036
- </ul>
1037
- </div>
1038
- )}
1039
- {isJobFinished && (
1040
- <div className="flex justify-end mt-2">
1041
- <Button onClick={() => {
1042
- setImportJobs(prevJobs => {
1043
- const newJobs = { ...prevJobs };
1044
- delete newJobs[job.jobId];
1045
- return newJobs;
1046
- });
1047
- setActiveJobIds(prevIds => prevIds.filter(id => id !== job.jobId));
1048
- }}><Trans i18nKey="btns.clear">Effacer</Trans></Button>
1049
- </div>
1050
- )}
1051
- </div>
1052
- );
1053
- })}
1054
- </div>
1055
- )}
1056
-
1057
- <div className="flex justify-end mt-4">
1058
- <Button onClick={handleCloseModal} disabled={isAnyImportInProgress}>
1059
- <Trans i18nKey="btns.close">Fermer</Trans>
1060
- </Button>
1061
- </div>
1062
- </>
1063
- </Dialog>
1064
- );
1065
- }
@@ -70,4 +70,21 @@
70
70
  .flex.justify-end.mt-4 button:disabled {
71
71
  background-color: #cccccc;
72
72
  cursor: not-allowed;
73
- }
73
+ }
74
+ .preview-table {
75
+ width: 100%;
76
+ border-collapse: collapse;
77
+ font-size: 0.9em;
78
+ }
79
+
80
+ .preview-table td {
81
+ border: 1px solid #ddd;
82
+ padding: 4px;
83
+ }
84
+
85
+ .preview-table-container {
86
+ max-height: 300px;
87
+ overflow: auto;
88
+ border: 1px solid #eee;
89
+ margin-bottom: 10px;
90
+ }
@@ -33,9 +33,7 @@ import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
33
33
 
34
34
  import { PhoneInput } from 'react-international-phone';
35
35
  import 'react-international-phone/style.css';
36
- import CodeMirror, {basicSetup} from "@uiw/react-codemirror";
37
36
  import {useAuthContext} from "./contexts/AuthContext.jsx";
38
- import {maxStringLength} from "data-primals-engine/constants";
39
37
 
40
38
  export const Form = ({
41
39
  name,
@@ -893,7 +891,6 @@ const FileField = ({ inputProps, value, onChange, name, mimeTypes, maxSize, mult
893
891
  }
894
892
  }, [value]);
895
893
 
896
- console.log(fileInfos)
897
894
  return (
898
895
  <div className="field field-file">
899
896
  <input
@@ -1414,58 +1411,98 @@ export const ModelField = ({field, disableable=false, showModel=true, value, fie
1414
1411
  const {models} = useModelContext();
1415
1412
  const {me} = useAuthContext();
1416
1413
  const {t} = useTranslation()
1417
- const [modelValue, setModelValue] = useState(value);
1418
- const [modelFieldValue, setModelFieldValue] = useState(null);
1419
1414
  const [checked, setChecked] = useState(true);
1420
1415
 
1421
- const itemsFields = [...models.find(f=>f.name === modelValue && me?.username === f._user)?.fields.map(m => ({label: m.name, value: m.name})) || []];
1416
+ // Trouver le modèle correspondant à la valeur
1417
+ const selectedModel = models.find(m => m.name === value && m._user === me?.username);
1422
1418
 
1423
- useEffect(() => {
1424
- console.log({value})
1425
- setModelValue(value)
1426
- }, [value]);
1419
+ // Préparer les options pour les champs du modèle
1420
+ const fieldOptions = selectedModel?.fields.map(f => ({
1421
+ label: t(`field_${f.name}`, f.name),
1422
+ value: f.name
1423
+ })) || [];
1427
1424
 
1428
- useEffect(() => {
1429
- console.log(modelValue)
1430
- onChange({name: field.name, value: modelValue});
1431
- }, [modelValue]);
1425
+ // Gestion du changement de modèle
1426
+ const handleModelChange = (e) => {
1427
+ const newModel = e.value;
1428
+ const firstField = fieldOptions[0]?.value || null;
1432
1429
 
1433
- useEffect(() => {
1434
- if( !fieldValue){
1435
- setModelFieldValue(itemsFields.length > 0 ? itemsFields[0].value : null)
1436
- }else
1437
- setModelFieldValue(fieldValue)
1438
- }, [fieldValue]);
1439
-
1440
- const dis = disableable ? <><CheckboxField checked={checked} onChange={e => {
1441
- setChecked(e.target.checked)
1442
- if (!e.target.checked) {
1443
- setModelValue(null);
1430
+ if (fields) {
1431
+ onChange({name: field?.name, value: { model: newModel, field: firstField }});
1432
+ } else {
1433
+ onChange({name: field?.name, value: newModel});
1444
1434
  }
1445
- }} /></> : null;
1446
-
1447
- if( !fields )
1448
- return <div className={"flex flex-1"}>
1449
- {dis}{checked && (<SelectField className="flex-1" value={modelValue} onChange={(e) => {
1450
- setModelValue(e.value)
1451
- onChange({name: field.name, value: e.value});
1452
- }} items={[...models.filter(f=>f._user === me?.username).map(m => ({label: t(`model_${m.name}`, m.name), value: m.name}))]}/>
1453
- )}
1454
- </div>;
1455
- return <div className={"flex flex-1"}>{dis}{checked && (<div className="flex flex-stretch" key={field?.name??'def'}>
1456
- {showModel && (<SelectField disabled={!!checked} className="flex-1" value={modelValue} onChange={(e) => {
1457
- setModelValue(e.value)
1458
- onChange({name: field.name, value: { model: e.value, field: itemsFields[0] }});
1459
- }} items={[...models.filter(f=>f._user === me?.username).map(m => ({label: t(`model_${m.name}`, m.name), value: m.name}))]}/>
1435
+ };
1436
+
1437
+ // Gestion du changement de champ
1438
+ const handleFieldChange = (e) => {
1439
+ onChange({name: field?.name, value: { model: value, field: e.value }});
1440
+ };
1441
+
1442
+ const dis = disableable ? (
1443
+ <CheckboxField
1444
+ checked={checked}
1445
+ onChange={e => {
1446
+ setChecked(e.target.checked);
1447
+ if (!e.target.checked) {
1448
+ onChange({name: field?.name, value: null});
1449
+ }
1450
+ }}
1451
+ />
1452
+ ) : null;
1453
+
1454
+ if (!fields) {
1455
+ return (
1456
+ <div className="flex flex-1">
1457
+ {dis}
1458
+ {checked && (
1459
+ <SelectField
1460
+ className="flex-1"
1461
+ value={value}
1462
+ onChange={handleModelChange}
1463
+ items={models
1464
+ .filter(m => m._user === me?.username)
1465
+ .map(m => ({
1466
+ label: t(`model_${m.name}`, m.name),
1467
+ value: m.name
1468
+ }))
1469
+ }
1470
+ />
1471
+ )}
1472
+ </div>
1473
+ );
1474
+ }
1475
+
1476
+ return (
1477
+ <div className="flex flex-1">
1478
+ {dis}
1479
+ {checked && (
1480
+ <div className="flex flex-stretch" key={field?.name ?? 'def'}>
1481
+ {showModel && (
1482
+ <SelectField
1483
+ className="flex-1"
1484
+ value={value}
1485
+ onChange={handleModelChange}
1486
+ items={models
1487
+ .filter(m => m._user === me?.username)
1488
+ .map(m => ({
1489
+ label: t(`model_${m.name}`, m.name),
1490
+ value: m.name
1491
+ }))
1492
+ }
1493
+ />
1494
+ )}
1495
+ <SelectField
1496
+ className="flex-1"
1497
+ value={fieldValue || (fieldOptions[0]?.value || null)}
1498
+ onChange={handleFieldChange}
1499
+ items={fieldOptions}
1500
+ />
1501
+ </div>
1460
1502
  )}
1461
- <SelectField className="flex-1" value={modelFieldValue} onChange={(e) =>{
1462
- setModelFieldValue(e.value)
1463
- onChange({name: field.name, value: { model: modelValue, field:e.value}});
1464
- }} items={itemsFields}/>
1465
- </div>)}
1466
- </div>
1467
- }
1468
- export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1503
+ </div>
1504
+ );
1505
+ };export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1469
1506
  // 1. État interne pour une réactivité immédiate de l'interface.
1470
1507
  const [internalValue, setInternalValue] = useState(value);
1471
1508
 
@@ -19,7 +19,7 @@ import {
19
19
  clearMappingsRecursive
20
20
  } from './FlexTreeUtils.js';
21
21
  import {Dialog, DialogProvider} from "./Dialog.jsx";
22
- import {safeAssignObject} from "data-primals-engine/core";
22
+ import {safeAssignObject} from "../../src/core";
23
23
 
24
24
  const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], lang = 'fr' }) => {
25
25
  const { me: user } = useAuthContext();
@@ -526,33 +526,37 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
526
526
  {/* Colonne de droite: Formulaire du modèle sélectionné */}
527
527
  <div className="model-form-container">
528
528
  {/* Le formulaire existant est placé ici */}
529
- <div className="flex field-bg">
530
- <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
529
+ <div className="field">
530
+ <div className="flex field-bg">
531
+ <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
532
+ </div>
533
+ <TextField
534
+ type="text"
535
+ id="modelName"
536
+ disabled={modelLocked}
537
+ value={modelName}
538
+ help={t('modelcreator.field.name.hint')}
539
+ onChange={(e) => setModelName(e.target.value)}
540
+ required
541
+ />
531
542
  </div>
532
- <TextField
533
- type="text"
534
- id="modelName"
535
- disabled={modelLocked}
536
- value={modelName}
537
- help={t('modelcreator.field.name.hint')}
538
- onChange={(e) => setModelName(e.target.value)}
539
- required
540
- />
541
-
542
- <div className="flex field-bg">
543
- <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
543
+
544
+ <div className="field">
545
+ <div className="flex field-bg">
546
+ <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
547
+ </div>
548
+ <TextField
549
+ multiline
550
+ help={t('modelcreator.field.description')}
551
+ id="modelDescription"
552
+ disabled={modelLocked}
553
+ value={modelDescription}
554
+ onChange={(e) => {
555
+ setModelDescription(e.target.value);
556
+ setChanged(true)
557
+ }}
558
+ />
544
559
  </div>
545
- <TextField
546
- multiline
547
- help={t('modelcreator.field.description')}
548
- id="modelDescription"
549
- disabled={modelLocked}
550
- value={modelDescription}
551
- onChange={(e) => {
552
- setModelDescription(e.target.value);
553
- setChanged(true)
554
- }}
555
- />
556
560
  <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
557
561
  {fields.map((field, index) => <ModelCreatorField
558
562
  key={initialModel?.name + '_field_' + index}