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.
- package/CONTRIBUTING.md +91 -0
- package/README.md +50 -22
- package/client/src/App.jsx +0 -5
- package/client/src/App.scss +6 -0
- package/client/src/ConditionBuilder.scss +34 -1
- package/client/src/ConditionBuilder2.jsx +179 -53
- package/client/src/ContentView.jsx +0 -3
- package/client/src/CronBuilder.jsx +0 -1
- package/client/src/CronPartBuilder.jsx +0 -2
- package/client/src/DashboardView.jsx +0 -5
- package/client/src/DataEditor.jsx +8 -10
- package/client/src/DataImporter.jsx +469 -0
- package/client/src/DataLayout.jsx +0 -1
- package/client/src/DataTable.jsx +2 -368
- package/client/src/DataTable.scss +18 -1
- package/client/src/Field.jsx +85 -48
- package/client/src/FlexBuilder.jsx +1 -1
- package/client/src/ModelCreator.jsx +29 -25
- package/client/src/ModelCreator.scss +13 -0
- package/client/src/ModelCreatorField.jsx +1 -5
- package/client/src/RTE.jsx +1 -6
- package/client/src/RTETrans.jsx +0 -2
- package/client/src/RelationField.jsx +1 -1
- package/client/src/RelationValue.jsx +1 -2
- package/client/src/TourSpotlight.jsx +0 -2
- package/client/src/filter.js +87 -0
- package/client/src/hooks/data.js +1 -3
- package/client/src/hooks/useTutorials.jsx +0 -1
- package/client/src/translations.js +60 -26
- package/package.json +4 -3
- package/server.js +2 -2
- package/src/data.js +8 -0
- package/src/email.js +2 -2
- package/src/engine.js +59 -20
- package/src/index.js +1 -1
- package/src/middlewares/middleware-mongodb.js +0 -1
- package/src/modules/assistant.js +1 -3
- package/src/modules/bucket.js +3 -4
- package/src/modules/data/data.core.js +17 -0
- package/src/modules/{data.js → data/data.js} +4595 -5991
- package/src/modules/data/data.routes.js +1637 -0
- package/src/modules/data/index.js +1 -0
- package/src/modules/file.js +1 -1
- package/src/modules/mongodb.js +0 -1
- package/src/modules/user.js +1 -1
- package/src/modules/workflow.js +38 -38
- package/src/packs.js +4 -1
- package/test/data.backup.integration.test.js +4 -5
- package/test/data.integration.test.js +2 -6
- package/test/events.test.js +1 -1
- package/test/file.test.js +1 -4
- package/test/import_export.integration.test.js +22 -15
- package/test/model.integration.test.js +8 -10
- package/test/user.test.js +2 -2
- package/test/vm.test.js +1 -1
- package/test/workflow.integration.test.js +17 -14
- package/test/workflow.robustness.test.js +15 -10
- package/src/modules/test +0 -147
package/client/src/DataTable.jsx
CHANGED
|
@@ -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 "
|
|
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
|
+
}
|
package/client/src/Field.jsx
CHANGED
|
@@ -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
|
-
|
|
1416
|
+
// Trouver le modèle correspondant à la valeur
|
|
1417
|
+
const selectedModel = models.find(m => m.name === value && m._user === me?.username);
|
|
1422
1418
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
{
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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 "
|
|
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="
|
|
530
|
-
<
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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}
|