data-primals-engine 1.2.4 → 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/README.md +17 -5
- package/client/src/App.scss +6 -0
- package/client/src/DataImporter.jsx +469 -0
- package/client/src/DataTable.jsx +1 -365
- package/client/src/DataTable.scss +18 -1
- package/client/src/Field.jsx +85 -43
- package/client/src/ModelCreator.jsx +29 -25
- package/client/src/ModelCreator.scss +13 -0
- package/client/src/translations.js +60 -26
- package/package.json +2 -1
- package/src/data.js +8 -0
- package/src/modules/data/data.core.js +17 -0
- package/src/modules/data/data.js +4595 -5974
- package/src/modules/data/data.routes.js +1637 -0
- package/test/import_export.integration.test.js +2 -2
package/client/src/DataTable.jsx
CHANGED
|
@@ -58,9 +58,7 @@ import {HiddenableCell} from "./HiddenableCell.jsx";
|
|
|
58
58
|
import ConditionBuilder from "./ConditionBuilder.jsx";
|
|
59
59
|
import {pagedFilterToMongoConds} from "./filter.js";
|
|
60
60
|
import {isConditionMet} from "../../src/filter";
|
|
61
|
-
|
|
62
|
-
// Ajoutez cette constante pour la clé de sessionStorage
|
|
63
|
-
const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
|
|
61
|
+
import {DataImporter} from "./DataImporter.jsx";
|
|
64
62
|
|
|
65
63
|
const Header = ({
|
|
66
64
|
reversed = false,
|
|
@@ -699,365 +697,3 @@ export function DataTable({
|
|
|
699
697
|
);
|
|
700
698
|
}
|
|
701
699
|
|
|
702
|
-
|
|
703
|
-
function DataImporter({onClose}) {
|
|
704
|
-
|
|
705
|
-
const [file, setFile] = useState(null);
|
|
706
|
-
const {selectedModel, page} = useModelContext();
|
|
707
|
-
|
|
708
|
-
const {me} = useAuthContext();
|
|
709
|
-
const {t, i18n} = useTranslation();
|
|
710
|
-
|
|
711
|
-
const queryClient = useQueryClient();
|
|
712
|
-
const {addNotification} = useNotificationContext();
|
|
713
|
-
|
|
714
|
-
const [hasHeaders, setHasHeaders] = useState(true);
|
|
715
|
-
const [csvHeaders, setCSVHeaders] = useState(selectedModel.fields.map(field => field.name));
|
|
716
|
-
|
|
717
|
-
// --- MODIFIÉ : État pour gérer plusieurs tâches d'importation ---
|
|
718
|
-
// Cet objet stockera les données de progression de chaque tâche, indexées par leur jobId
|
|
719
|
-
const [importJobs, setImportJobs] = useState({});
|
|
720
|
-
// Cette liste stockera les IDs des tâches qui sont actuellement suivies via SSE
|
|
721
|
-
const [activeJobIds, setActiveJobIds] = useState([]);
|
|
722
|
-
|
|
723
|
-
// Référence pour stocker les instances EventSource, indexées par jobId
|
|
724
|
-
const eventSourceRefs = useRef({});
|
|
725
|
-
|
|
726
|
-
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
727
|
-
|
|
728
|
-
const [storedJobIds, setStoredJobIds] = useLocalStorage(SESSION_STORAGE_IMPORT_JOBS_KEY, []);
|
|
729
|
-
// --- NOUVEAU : Charger/Sauvegarder les IDs des tâches actives depuis sessionStorage ---
|
|
730
|
-
useEffect(() => {
|
|
731
|
-
// Charger les IDs des tâches actives depuis sessionStorage au montage
|
|
732
|
-
setActiveJobIds(storedJobIds);
|
|
733
|
-
|
|
734
|
-
// Pour chaque jobId stockpour obtenir le dernier statut
|
|
735
|
-
storedJobIds.forEach(jobId => {
|
|
736
|
-
startProgressTracking(jobId);
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// Fonction de nettoyage : fermer toutes les connexions EventSource lors du démontage du composant
|
|
740
|
-
return () => {
|
|
741
|
-
Object.values(eventSourceRefs.current).forEach(es => es.close());
|
|
742
|
-
eventSourceRefs.current = {}; // Effacer les références
|
|
743
|
-
};
|
|
744
|
-
}, []); // S'exécute une seule fois au montage
|
|
745
|
-
|
|
746
|
-
// --- NOUVEAU : Effet pour mettre à jour sessionStorage lorsque activeJobIds change ---
|
|
747
|
-
useEffect(() => {
|
|
748
|
-
setStoredJobIds(activeJobIds);
|
|
749
|
-
}, [activeJobIds]);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
// Mutation pour initier l'importation (envoi du fichier au serveur)
|
|
753
|
-
const {isLoading, mutate: importMutation} = useMutation(async () => {
|
|
754
|
-
console.log('Initiating data import...');
|
|
755
|
-
const params = new FormData();
|
|
756
|
-
params.append('model', selectedModel?.name);
|
|
757
|
-
params.append("_user", getUserId(me));
|
|
758
|
-
params.append("hasHeaders", !!hasHeaders);
|
|
759
|
-
params.append("csvHeaders", csvHeaders.join(','));
|
|
760
|
-
if (file) {
|
|
761
|
-
params.append("file", file);
|
|
762
|
-
} else {
|
|
763
|
-
addNotification({ title: t('dataimporter.noFileSelected', 'Veuillez sélectionner un fichier à importer.'), status: 'warning' });
|
|
764
|
-
return Promise.reject(new Error("No file selected"));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
try {
|
|
768
|
-
const response = await fetch(`/api/data/import?lang=${lang}`, {
|
|
769
|
-
method: 'POST',
|
|
770
|
-
body: params
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
if (response.status === 202) {
|
|
774
|
-
const { job } = await response.json();
|
|
775
|
-
const { jobId} = job;
|
|
776
|
-
|
|
777
|
-
// --- MODIFIÉ : Ajouter le nouvel jobId à activeJobIds et à l'état importJobs ---
|
|
778
|
-
setActiveJobIds(prevIds => [...prevIds, jobId]);
|
|
779
|
-
setImportJobs(prevJobs => ({
|
|
780
|
-
...prevJobs,
|
|
781
|
-
[jobId]: {
|
|
782
|
-
jobId,
|
|
783
|
-
status: 'pending',
|
|
784
|
-
totalRecords: 0,
|
|
785
|
-
processedRecords: 0,
|
|
786
|
-
errors: [],
|
|
787
|
-
// Ajoutez d'autres champs initiaux que vous souhaitez afficher immédiatement
|
|
788
|
-
}
|
|
789
|
-
}));
|
|
790
|
-
startProgressTracking(jobId); // Commencer le suivi de cette nouvelle tâche
|
|
791
|
-
addNotification({
|
|
792
|
-
title: t('dataimporter.initiated', 'Importation initiée. Suivi de la progression...'),
|
|
793
|
-
icon: <FaInfo/>,
|
|
794
|
-
status: 'info'
|
|
795
|
-
});
|
|
796
|
-
} else {
|
|
797
|
-
const errorData = await response.json();
|
|
798
|
-
addNotification({
|
|
799
|
-
title: errorData.error || t('dataimporter.error', 'Erreur lors de l\'importation.'),
|
|
800
|
-
status: 'error'
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
} catch (e) {
|
|
804
|
-
addNotification({
|
|
805
|
-
title: e.message || t('dataimporter.networkError', 'Erreur réseau lors de l\'importation.'),
|
|
806
|
-
status: 'error'
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
// Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
|
|
812
|
-
|
|
813
|
-
// Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
|
|
814
|
-
const startProgressTracking = (jobId) => {
|
|
815
|
-
// Fermer toute connexion EventSource existante pour ce jobId pour éviter les doublons
|
|
816
|
-
if (eventSourceRefs.current[jobId]) {
|
|
817
|
-
eventSourceRefs.current[jobId].close();
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const eventSource = new EventSource(`/api/import/progress/${jobId}`);
|
|
821
|
-
eventSourceRefs.current[jobId] = eventSource; // Stocker l'instance pour le nettoyage
|
|
822
|
-
|
|
823
|
-
eventSource.onmessage = (event) => {
|
|
824
|
-
const data = JSON.parse(event.data);
|
|
825
|
-
// --- MODIFIÉ : Mettre à jour la progression de la tâche spécifique ---
|
|
826
|
-
setImportJobs(prevJobs => ({
|
|
827
|
-
...prevJobs,
|
|
828
|
-
[jobId]: data
|
|
829
|
-
}));
|
|
830
|
-
|
|
831
|
-
// Si la tâche est terminée (succès ou échec), fermer la connexion SSE.
|
|
832
|
-
// NE PAS la retirer de activeJobIds ici, pour qu'elle persiste au rafraîchissement.
|
|
833
|
-
// Elle sera retirar le bouton "Effacer".
|
|
834
|
-
if (data.status === 'completed' || data.status === 'failed' || data.status === 'not_found') {
|
|
835
|
-
eventSource.close();
|
|
836
|
-
delete eventSourceRefs.current[jobId]; // Supprimer la référence de l'EventSource
|
|
837
|
-
|
|
838
|
-
// --- LIGNE MODIFIÉE/SUPPRIMÉE ---
|
|
839
|
-
// Supprimez ou commentez la ligne suivante :
|
|
840
|
-
// setActiveJobIds(prevIds => prevIds.filter(id => id !== jobId));
|
|
841
|
-
|
|
842
|
-
queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page]); // Rafraîchir les données du tableau
|
|
843
|
-
|
|
844
|
-
if (data.status === 'completed') {
|
|
845
|
-
addNotification({
|
|
846
|
-
title: t('dataimporter.success', 'Importation des données réussie.'),
|
|
847
|
-
icon: <FaInfo/>,
|
|
848
|
-
status: 'completed'
|
|
849
|
-
});
|
|
850
|
-
} else if (data.status === 'failed') {
|
|
851
|
-
addNotification({
|
|
852
|
-
title: t('dataimporter.failed', 'Importation échouée. Voir les détails pour les erreurs.'),
|
|
853
|
-
status: 'error'
|
|
854
|
-
});
|
|
855
|
-
} else if (data.status === 'not_found') {
|
|
856
|
-
addNotification({
|
|
857
|
-
title: t('dataimporter.jobNotFound', 'Tâche d\'importation non trouvée ou déjà terminée.'),
|
|
858
|
-
status: 'warning'
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
};
|
|
863
|
-
|
|
864
|
-
eventSource.onerror = (error) => {
|
|
865
|
-
console.error(`EventSource error for job ${jobId}:`, error);
|
|
866
|
-
eventSource.close();
|
|
867
|
-
delete eventSourceRefs.current[jobId];
|
|
868
|
-
setStoredJobIds(prevIds => prevIds.filter(id => id !== jobId));
|
|
869
|
-
};
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
const handleImportClick = () => {
|
|
873
|
-
importMutation();
|
|
874
|
-
};
|
|
875
|
-
|
|
876
|
-
const handleCloseModal = () => {
|
|
877
|
-
// Fermer toutes les connexions EventSource avant de fermer la modale
|
|
878
|
-
Object.values(eventSourceRefs.current).forEach(es => es.close());
|
|
879
|
-
eventSourceRefs.current = {}; // Effacer les références
|
|
880
|
-
onClose();
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
// Déterminer si une importation est actuellement en cours (pour désactiver les boutons)
|
|
884
|
-
const isAnyImportInProgress = Object.values(importJobs).some(job => job.status === 'pending' || job.status === 'processing');
|
|
885
|
-
|
|
886
|
-
// Filtrer et trier les tâches à afficher (par exemple, les tâches en cours en premier)
|
|
887
|
-
const jobsToDisplay = Object.values(importJobs).sort((a, b) => {
|
|
888
|
-
// Trier par statut (en attente/en cours en premier, puis échoué, puis terminé)
|
|
889
|
-
const statusOrder = { 'pending': 1, 'processing': 2, 'failed': 3, 'completed': 4, 'not_found': 5 };
|
|
890
|
-
return statusOrder[a.status] - statusOrder[b.status];
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
return (
|
|
894
|
-
<Dialog isClosable={true} isModal={true} onClose={handleCloseModal}>
|
|
895
|
-
<>
|
|
896
|
-
<h2>
|
|
897
|
-
<Trans i18nKey="dataimporter.title" values={{model: t('model_' + selectedModel?.name, selectedModel?.name)}}>
|
|
898
|
-
Importer des données dans {t('model_' + selectedModel?.name, selectedModel?.name)}
|
|
899
|
-
</Trans>
|
|
900
|
-
</h2>
|
|
901
|
-
<p>
|
|
902
|
-
<Trans i18nKey="dataimporter.info" values={{constante: (maxBytesPerSecondThrottleData / kilobytes) + 'ko/s'}}></Trans>
|
|
903
|
-
</p>
|
|
904
|
-
|
|
905
|
-
{/* Toujours afficher le formulaire de sélection de fichier et le bouton d'importation */}
|
|
906
|
-
<FileField
|
|
907
|
-
name="file"
|
|
908
|
-
maxSize={maxFileSize}
|
|
909
|
-
mimeTypes={['application/json', 'text/csv']}
|
|
910
|
-
type="file"
|
|
911
|
-
multiple={false}
|
|
912
|
-
onChange={(files) => {
|
|
913
|
-
setFile(files && files.length > 0 ? files[files.length - 1].file : null);
|
|
914
|
-
}}
|
|
915
|
-
/>
|
|
916
|
-
{file && (file.name.endsWith('.csv') || file.type === 'text/csv') && (
|
|
917
|
-
<div className="checkbox-label flex flex-row">
|
|
918
|
-
<label htmlFor="hasHeadersCheckbox">
|
|
919
|
-
<input
|
|
920
|
-
type="checkbox"
|
|
921
|
-
id="hasHeadersCheckbox"
|
|
922
|
-
checked={hasHeaders}
|
|
923
|
-
onChange={(e) => setHasHeaders(e.target.checked)}
|
|
924
|
-
/>
|
|
925
|
-
<Trans i18nKey="dataimporter.hasCsvHeaders"></Trans>
|
|
926
|
-
</label>
|
|
927
|
-
{!hasHeaders && (
|
|
928
|
-
<table>
|
|
929
|
-
<thead>
|
|
930
|
-
<tr>
|
|
931
|
-
<th><Trans i18nKey={"dataimporter.csvColumn"}>Num de colonne du CSV</Trans></th>
|
|
932
|
-
<th><Trans i18nKey="dataimporter.field">Champ du modèle</Trans></th>
|
|
933
|
-
</tr>
|
|
934
|
-
</thead>
|
|
935
|
-
<tbody>
|
|
936
|
-
{csvHeaders.map((mappedFieldName, index) => {
|
|
937
|
-
let fieldObjectForModelField = null;
|
|
938
|
-
let currentFieldValueForModelField = null;
|
|
939
|
-
|
|
940
|
-
if (mappedFieldName && mappedFieldName.trim() !== '') {
|
|
941
|
-
fieldObjectForModelField = selectedModel?.fields.find(f_model => f_model.name === mappedFieldName);
|
|
942
|
-
if (fieldObjectForModelField) {
|
|
943
|
-
currentFieldValueForModelField = fieldObjectForModelField.name;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
return (
|
|
948
|
-
<tr key={`${selectedModel.name}-csvmap-${index}`}>
|
|
949
|
-
<td><Trans i18nKey="dataimporter.column" values={[index + 1]}>colonne {index + 1}</Trans></td>
|
|
950
|
-
<td>
|
|
951
|
-
<div className="flex">
|
|
952
|
-
<ModelField
|
|
953
|
-
disableable={true}
|
|
954
|
-
showModel={false}
|
|
955
|
-
value={selectedModel.name}
|
|
956
|
-
fieldValue={currentFieldValueForModelField}
|
|
957
|
-
onChange={({name: propName, value: selectedValue}) => {
|
|
958
|
-
const newCsvHeaders = [...csvHeaders];
|
|
959
|
-
newCsvHeaders[index] = selectedValue?.field ?? '';
|
|
960
|
-
setCSVHeaders(newCsvHeaders);
|
|
961
|
-
}}
|
|
962
|
-
fields={true}
|
|
963
|
-
model={selectedModel}
|
|
964
|
-
field={fieldObjectForModelField}
|
|
965
|
-
/>
|
|
966
|
-
<Button className="flex" onClick={() => {
|
|
967
|
-
const csvH = [...csvHeaders];
|
|
968
|
-
setCSVHeaders(csvH.filter((_, i) => i !== index));
|
|
969
|
-
}}><FaTrash/></Button>
|
|
970
|
-
</div>
|
|
971
|
-
</td>
|
|
972
|
-
</tr>
|
|
973
|
-
);
|
|
974
|
-
})}
|
|
975
|
-
<tr>
|
|
976
|
-
<td colSpan={2}>
|
|
977
|
-
<Button onClick={() => {
|
|
978
|
-
const csvH = [...csvHeaders];
|
|
979
|
-
csvH.push('');
|
|
980
|
-
setCSVHeaders(csvH)
|
|
981
|
-
}}><Trans i18nKey="dataimporter.addColumn">Ajouter une colonne</Trans></Button>
|
|
982
|
-
</td>
|
|
983
|
-
</tr>
|
|
984
|
-
</tbody>
|
|
985
|
-
</table>
|
|
986
|
-
)}
|
|
987
|
-
</div>
|
|
988
|
-
)}
|
|
989
|
-
<div>
|
|
990
|
-
<Button onClick={handleImportClick} disabled={isLoading || !file}>
|
|
991
|
-
<Trans i18nKey="btns.import">Importer</Trans>
|
|
992
|
-
</Button>
|
|
993
|
-
</div>
|
|
994
|
-
|
|
995
|
-
{/* Afficher la progression pour toutes les tâches d'importation actives/suivies */}
|
|
996
|
-
{jobsToDisplay.length > 0 && (
|
|
997
|
-
<div className="import-jobs-list">
|
|
998
|
-
<h3><Trans i18nKey="dataimporter.activeImports">Importations en cours / terminées</Trans></h3>
|
|
999
|
-
{jobsToDisplay.map(job => {
|
|
1000
|
-
const progressPercentage = job.totalRecords > 0
|
|
1001
|
-
? (job.processedRecords / job.totalRecords) * 100
|
|
1002
|
-
: 0;
|
|
1003
|
-
const isJobFinished = job.status === 'completed' || job.status === 'failed' || job.status === 'not_found';
|
|
1004
|
-
|
|
1005
|
-
return (
|
|
1006
|
-
<div key={job.jobId} className="import-progress-container">
|
|
1007
|
-
<h4><Trans i18nKey="dataimporter.jobId">Tâche ID:</Trans> {job.jobId?.substring(0, 8)}...</h4>
|
|
1008
|
-
<p>
|
|
1009
|
-
<Trans i18nKey="dataimporter.status">Statut:</Trans>{' '}
|
|
1010
|
-
<strong>{t(`dataimporter.status.${job.status}`, job.status)}</strong>
|
|
1011
|
-
</p>
|
|
1012
|
-
{job.totalRecords > 0 && (
|
|
1013
|
-
<p>
|
|
1014
|
-
<Trans i18nKey="dataimporter.recordsProcessed">Enregistrements traités:</Trans>{' '}
|
|
1015
|
-
{job.processedRecords} / {job.totalRecords}
|
|
1016
|
-
</p>
|
|
1017
|
-
)}
|
|
1018
|
-
<div className="progress-bar-wrapper">
|
|
1019
|
-
<div
|
|
1020
|
-
className="progress-bar"
|
|
1021
|
-
style={{ width: `${progressPercentage}%` }}
|
|
1022
|
-
>
|
|
1023
|
-
{progressPercentage.toFixed(0)}%
|
|
1024
|
-
</div>
|
|
1025
|
-
</div>
|
|
1026
|
-
|
|
1027
|
-
{job.errors && job.errors.length > 0 && (
|
|
1028
|
-
<div className="import-errors">
|
|
1029
|
-
<h4><Trans i18nKey="dataimporter.errors">Erreurs:</Trans></h4>
|
|
1030
|
-
<ul>
|
|
1031
|
-
{job.errors.map((error, index) => (
|
|
1032
|
-
<li key={index}>{error}</li>
|
|
1033
|
-
))}
|
|
1034
|
-
</ul>
|
|
1035
|
-
</div>
|
|
1036
|
-
)}
|
|
1037
|
-
{isJobFinished && (
|
|
1038
|
-
<div className="flex justify-end mt-2">
|
|
1039
|
-
<Button onClick={() => {
|
|
1040
|
-
setImportJobs(prevJobs => {
|
|
1041
|
-
const newJobs = { ...prevJobs };
|
|
1042
|
-
delete newJobs[job.jobId];
|
|
1043
|
-
return newJobs;
|
|
1044
|
-
});
|
|
1045
|
-
setActiveJobIds(prevIds => prevIds.filter(id => id !== job.jobId));
|
|
1046
|
-
}}><Trans i18nKey="btns.clear">Effacer</Trans></Button>
|
|
1047
|
-
</div>
|
|
1048
|
-
)}
|
|
1049
|
-
</div>
|
|
1050
|
-
);
|
|
1051
|
-
})}
|
|
1052
|
-
</div>
|
|
1053
|
-
)}
|
|
1054
|
-
|
|
1055
|
-
<div className="flex justify-end mt-4">
|
|
1056
|
-
<Button onClick={handleCloseModal} disabled={isAnyImportInProgress}>
|
|
1057
|
-
<Trans i18nKey="btns.close">Fermer</Trans>
|
|
1058
|
-
</Button>
|
|
1059
|
-
</div>
|
|
1060
|
-
</>
|
|
1061
|
-
</Dialog>
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
@@ -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
|
@@ -1411,56 +1411,98 @@ export const ModelField = ({field, disableable=false, showModel=true, value, fie
|
|
|
1411
1411
|
const {models} = useModelContext();
|
|
1412
1412
|
const {me} = useAuthContext();
|
|
1413
1413
|
const {t} = useTranslation()
|
|
1414
|
-
const [modelValue, setModelValue] = useState(value);
|
|
1415
|
-
const [modelFieldValue, setModelFieldValue] = useState(null);
|
|
1416
1414
|
const [checked, setChecked] = useState(true);
|
|
1417
1415
|
|
|
1418
|
-
|
|
1416
|
+
// Trouver le modèle correspondant à la valeur
|
|
1417
|
+
const selectedModel = models.find(m => m.name === value && m._user === me?.username);
|
|
1419
1418
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
+
})) || [];
|
|
1423
1424
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1425
|
+
// Gestion du changement de modèle
|
|
1426
|
+
const handleModelChange = (e) => {
|
|
1427
|
+
const newModel = e.value;
|
|
1428
|
+
const firstField = fieldOptions[0]?.value || null;
|
|
1427
1429
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
setModelFieldValue(fieldValue)
|
|
1433
|
-
}, [fieldValue]);
|
|
1434
|
-
|
|
1435
|
-
const dis = disableable ? <><CheckboxField checked={checked} onChange={e => {
|
|
1436
|
-
setChecked(e.target.checked)
|
|
1437
|
-
if (!e.target.checked) {
|
|
1438
|
-
setModelValue(null);
|
|
1430
|
+
if (fields) {
|
|
1431
|
+
onChange({name: field?.name, value: { model: newModel, field: firstField }});
|
|
1432
|
+
} else {
|
|
1433
|
+
onChange({name: field?.name, value: newModel});
|
|
1439
1434
|
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
{
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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>
|
|
1455
1502
|
)}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}} items={itemsFields}/>
|
|
1460
|
-
</div>)}
|
|
1461
|
-
</div>
|
|
1462
|
-
}
|
|
1463
|
-
export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
|
|
1503
|
+
</div>
|
|
1504
|
+
);
|
|
1505
|
+
};export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
|
|
1464
1506
|
// 1. État interne pour une réactivité immédiate de l'interface.
|
|
1465
1507
|
const [internalValue, setInternalValue] = useState(value);
|
|
1466
1508
|
|
|
@@ -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}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
.model-creator {
|
|
2
2
|
flex: 1;
|
|
3
|
+
width: 100%;
|
|
3
4
|
max-width: 720px;
|
|
4
5
|
form {
|
|
5
6
|
padding: 0;
|
|
@@ -31,6 +32,9 @@
|
|
|
31
32
|
border-radius: 8px;
|
|
32
33
|
padding: 16px 8px;
|
|
33
34
|
@media only screen and (min-width: 768px) {
|
|
35
|
+
padding: 1rem; // Plus d'espace intérieur
|
|
36
|
+
}
|
|
37
|
+
@media only screen and (min-width: 1024px) {
|
|
34
38
|
padding: 1.5rem; // Plus d'espace intérieur
|
|
35
39
|
}
|
|
36
40
|
margin-bottom: 1.5rem; // Plus d'espace entre les champs
|
|
@@ -129,8 +133,17 @@
|
|
|
129
133
|
|
|
130
134
|
.model-form-container {
|
|
131
135
|
flex: 1; // Le formulaire prend le reste de la place
|
|
136
|
+
> .field {
|
|
137
|
+
box-sizing: border-box;
|
|
138
|
+
margin-bottom: 28px;
|
|
139
|
+
}
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
.actions.right {
|
|
143
|
+
margin-right: 0;
|
|
144
|
+
margin-left: auto;
|
|
145
|
+
width: fit-content;
|
|
146
|
+
}
|
|
134
147
|
// Styles pour la zone de prompt de l'IA
|
|
135
148
|
.ai-prompt-container {
|
|
136
149
|
background-color: #f8f9fa;
|