Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

front: improve import view #10139

Merged
merged 5 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
"error": "An error has occurred",
"errorEmptyFile": "Empty file",
"errorImport": "Unable to convert data to TrainSchedule",
"errorInvalidJSONFormat": "Invalid JSON format",
"errorInvalidXMLFormat": "Invalid XML format",
"errorNoDate": "You must enter a date.",
"errorNoFrom": "You must fill in an origin.",
"errorNoTo": "You must fill in a destination.",
"errorSameFromTo": "Origin and destination must be different",
"errorUnsupportedFileType": "Unsupported file type"
"errorInvalidFile": "Invalid file"
},
"failure": "Operation failed",
"from": "Origin",
Expand All @@ -28,9 +26,10 @@
"selectStation": "Select this station",
"startTime": "START",
"status": {
"calculatingTrainScheduleCompleteAllSuccess_one": "A train is imported.",
"calculatingTrainScheduleCompleteAllSuccess": "All trains are imported.",
"calculatingTrainScheduleCompleteAllFailure": "All running time calculations have failed."
"invalidTrainSchedules_one": "Invalid train schedule.",
"invalidTrainSchedules": "Invalid train schedules.",
"successfulImport_one": "A train is imported.",
"successfulImport": "All trains are imported."
},
"success": "Operation successful",
"to": "Destination",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
"error": "Une erreur est survenue",
"errorEmptyFile": "Fichier vide",
"errorImport": "Impossible de convertir les données en TrainSchedule",
"errorInvalidJSONFormat": "Format JSON invalide",
"errorInvalidXMLFormat": "Format XML invalide",
"errorNoDate": "Vous devez renseigner une date.",
"errorNoFrom": "Vous devez renseigner une origine.",
"errorNoTo": "Vous devez renseigner une destination.",
"errorSameFromTo": "L'origine et la destination doivent être différentes.",
"errorUnsupportedFileType": "Type de fichier non supporté"
"errorInvalidFile": "Fichier invalide"
},
"failure": "Opération échouée",
"from": "Origine",
Expand All @@ -28,11 +26,12 @@
"selectStation": "Sélectionner cette station",
"startTime": "DÉBUT",
"status": {
"calculatingTrainScheduleCompleteAllSuccess_one": "Le train a été importé.",
"calculatingTrainScheduleCompleteAllSuccess": "Tous les trains ont été importés.",
"calculatingTrainScheduleCompleteAllFailure": "Tous les calculs de marche ont échoué."
"invalidTrainSchedules_one": "La circulation à importer n'est pas au bon format.",
"invalidTrainSchedules": "Les circulations à importer ne sont pas au bon format.",
"successfulImport_one": "Le train a été importé.",
"successfulImport": "Tous les trains ont été importés."
},
"success": "Opération réussie",
"to": "Destination",
clarani marked this conversation as resolved.
Show resolved Hide resolved
"trainsFound": "trains trouvé(s)"
"trainsFound": "train(s) trouvé(s)"
}
58 changes: 4 additions & 54 deletions front/src/common/uploadFileModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useContext, useState } from 'react';
import { useContext, useState } from 'react';

import { Download } from '@osrd-project/ui-icons';
import { isNil } from 'lodash';
Expand All @@ -16,50 +16,6 @@ const UploadFileModal = ({ handleSubmit }: UploadFileModalProps) => {
const { t } = useTranslation(['operationalStudies/importTrainSchedule']);
const { closeModal } = useContext(ModalContext);
const [selectedFile, setSelectedFile] = useState<File | undefined>(undefined);
const [isValid, setIsValid] = useState<string | undefined>(undefined);

const parseXML = (xmlString: string) => {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
const parserError = xmlDoc.getElementsByTagName('parsererror');
if (parserError.length > 0) {
throw new Error('Invalid XML');
}
return undefined;
} catch (error) {
return t('errorMessages.errorInvalidXMLFormat').toString();
}
};
// TODO : create the translation keys
const validateFile = useCallback(
async (fileToValidate: File): Promise<string | undefined> => {
if (fileToValidate.size === 0) {
return t('errorMessages.errorEmptyFile').toString();
}
if (fileToValidate.type === 'application/json') {
try {
JSON.parse(await fileToValidate.text());
} catch (e) {
return t('errorMessages.errorInvalidJSONFormat').toString();
}
} else if (
fileToValidate.type === 'application/railml' ||
fileToValidate.name.endsWith('.railml')
) {
const fileContent = await fileToValidate.text();
const xmlError = parseXML(fileContent);
if (xmlError) {
return xmlError;
}
} else {
return t('errorMessages.errorUnsupportedFileType').toString();
}

return undefined;
},
[t]
);

return (
<>
Expand All @@ -73,21 +29,15 @@ const UploadFileModal = ({ handleSubmit }: UploadFileModalProps) => {
<input
type="file"
name="file"
accept=".json,.xml,.railml"
accept=".json,.txt,.xml,.railml"
onChange={async (e) => {
if (e.target.files && e.target.files.length > 0) {
const error = await validateFile(e.target.files[0]);
setIsValid(error);
if (isNil(error)) {
setSelectedFile(e.target.files[0]);
}
setSelectedFile(e.target.files[0]);
} else {
setSelectedFile(undefined);
setIsValid(undefined);
}
}}
/>
{!isNil(isValid) && <div className="text-danger">{isValid}</div>}
</>
</ModalBodySNCF>
<ModalFooterSNCF>
Expand All @@ -105,7 +55,7 @@ const UploadFileModal = ({ handleSubmit }: UploadFileModalProps) => {
<div className="col-6">
<button
type="button"
disabled={isNil(selectedFile) || !isNil(isValid)}
disabled={isNil(selectedFile)}
className="btn btn-block btn-sm btn-primary"
onClick={() => {
if (selectedFile) handleSubmit(selectedFile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { formatIsoDate } from 'utils/date';

import {
handleFileReadingError,
handleUnsupportedFileType,
processJsonFile,
processXmlFile,
} from '../ManageTrainSchedule/helpers/handleParseFiles';
Expand Down Expand Up @@ -320,21 +319,38 @@ const ImportTrainScheduleConfig = ({
closeModal();
setTrainsList([]);

const fileName = file.name.toLowerCase();
const fileExtension = fileName.split('.').pop();

let fileContent: string;
try {
const fileContent = await file.text();

if (fileExtension === 'json') {
processJsonFile(fileContent, setTrainsJsonData, dispatch);
} else if (fileExtension === 'xml' || fileExtension === 'railml') {
processXmlFile(fileContent, parseRailML, updateTrainSchedules, dispatch);
} else {
handleUnsupportedFileType(dispatch);
}
fileContent = await file.text();
} catch (error) {
handleFileReadingError(error as Error);
return;
}

const fileHasBeenParsed = processJsonFile(
fileContent,
file.type,
setTrainsJsonData,
dispatch,
t
);

// the file has been processed, return
if (fileHasBeenParsed) {
return;
}

// try to parse the file as an XML file
try {
await processXmlFile(fileContent, parseRailML, updateTrainSchedules);
} catch {
// the file is not supported or is an invalid XML file
dispatch(
setFailure({
name: t('errorMessages.error'),
message: t('errorMessages.errorInvalidFile'),
})
);
}
};
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,19 @@ const ImportTrainScheduleTrainsList = ({
dispatch(
setSuccess({
title: t('success'),
text: t('status.calculatingTrainScheduleCompleteAllSuccess', {
text: t('status.successfulImport', {
trainsList,
count: trainsList.length,
count: trainsList.length || trainsJsonData.length,
}),
})
);
} catch (error) {
dispatch(
setFailure({
name: t('failure'),
message: t('status.calculatingTrainScheduleCompleteAllFailure', {
message: t('status.invalidTrainSchedules', {
trainsList,
count: trainsList.length,
count: trainsList.length || trainsJsonData.length,
}),
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { t } from 'i18next';
import type { TFunction } from 'i18next';
import type { Dispatch } from 'redux';

import type { ImportedTrainSchedule } from 'applications/operationalStudies/types';
Expand All @@ -9,71 +9,100 @@ export const handleFileReadingError = (error: Error) => {
console.error('File reading error:', error);
};

export const handleJsonParsingError = (error: Error, dispatch: Dispatch) => {
console.error('Error parsing JSON:', error);
dispatch(
setFailure({
name: t('errorMessages.error'),
message: t('errorMessages.errorInvalidJSONFormat'),
})
);
};
const TRAIN_SCHEDULE_COMPULSORY_KEYS: (keyof TrainScheduleBase)[] = [
'constraint_distribution',
'path',
'rolling_stock_name',
'start_time',
'train_name',
];

const validateTrainSchedules = (
importedTrainSchedules: Partial<TrainScheduleBase>[]
): TrainScheduleBase[] => {
const isInvalidTrainSchedules = importedTrainSchedules.some((trainSchedule) => {
if (
TRAIN_SCHEDULE_COMPULSORY_KEYS.some((key) => !(key in trainSchedule)) ||
!Array.isArray(trainSchedule.path)
) {
return true;
}
const hasInvalidSteps = trainSchedule.path.some((step) => !('id' in step));
return hasInvalidSteps;
});

export const handleXmlParsingError = (error: Error, dispatch: Dispatch) => {
console.error('Error parsing XML/RailML:', error);
dispatch(
setFailure({
name: t('errorMessages.error'),
message: t('errorMessages.errorInvalidXMLFormat'),
})
);
if (isInvalidTrainSchedules) {
throw new Error('Invalid train schedules: some compulsory keys are missing');
}
return importedTrainSchedules as TrainScheduleBase[];
};

export const processJsonFile = (
fileContent: string,
fileExtension: string,
setTrainsJsonData: (data: TrainScheduleBase[]) => void,
dispatch: Dispatch
dispatch: Dispatch,
t: TFunction
) => {
const isJsonFile = fileExtension === 'application/json';

// try to parse the file content
let rawContent;
try {
rawContent = JSON.parse(fileContent);
} catch {
if (isJsonFile) {
dispatch(
setFailure({
name: t('errorMessages.error'),
message: t('errorMessages.errorInvalidFile'),
})
);
}
return isJsonFile;
}

// validate the trainSchedules
try {
const importedTrainSchedules: TrainScheduleBase[] = JSON.parse(fileContent);
if (importedTrainSchedules && importedTrainSchedules.length > 0) {
const importedTrainSchedules = validateTrainSchedules(rawContent);
if (importedTrainSchedules.length > 0) {
setTrainsJsonData(importedTrainSchedules);
} else {
dispatch(
setFailure({
name: t('errorMessages.error'),
message: t('errorMessages.errorEmptyFile'),
})
);
}
} catch (error) {
handleJsonParsingError(error as Error, dispatch);
} catch {
dispatch(
setFailure({
name: t('errorMessages.error'),
message: t('errorMessages.errorInvalidFile'),
})
);
}

// file has been parsed successfully
return true;
};

export const processXmlFile = async (
fileContent: string,
clarani marked this conversation as resolved.
Show resolved Hide resolved
parseRailML: (xmlDoc: Document) => Promise<ImportedTrainSchedule[]>,
updateTrainSchedules: (schedules: ImportedTrainSchedule[]) => void,
dispatch: Dispatch
updateTrainSchedules: (schedules: ImportedTrainSchedule[]) => void
) => {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(fileContent, 'application/xml');
const parserError = xmlDoc.getElementsByTagName('parsererror');

if (parserError.length > 0) {
throw new Error('Invalid XML');
}
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(fileContent, 'application/xml');
const parserError = xmlDoc.getElementsByTagName('parsererror');

const importedTrainSchedules = await parseRailML(xmlDoc);
if (importedTrainSchedules && importedTrainSchedules.length > 0) {
updateTrainSchedules(importedTrainSchedules);
}
} catch (error) {
handleXmlParsingError(error as Error, dispatch);
if (parserError.length > 0) {
throw new Error('Invalid XML');
}
SharglutDev marked this conversation as resolved.
Show resolved Hide resolved
clarani marked this conversation as resolved.
Show resolved Hide resolved
};

export const handleUnsupportedFileType = (dispatch: Dispatch) => {
console.error('Unsupported file type');
dispatch(
setFailure({
name: t('errorMessages.error'),
message: t('errorMessages.errorUnsupportedFileType'),
})
);
const importedTrainSchedules = await parseRailML(xmlDoc);
if (importedTrainSchedules && importedTrainSchedules.length > 0) {
updateTrainSchedules(importedTrainSchedules);
}
};
Loading