From 65f7fd5546ee55ce21dc760d0fd108c1c1f6e8e3 Mon Sep 17 00:00:00 2001 From: SharglutDev Date: Tue, 1 Oct 2024 18:21:53 +0200 Subject: [PATCH] dynamic modal --- .../applications/operationalStudies/Home.tsx | 32 ++++++++- .../operationalStudies/views/Project.tsx | 66 ++++++++++------- .../views/SimulationResults.tsx | 11 ++- .../operationalStudies/views/Study.tsx | 17 ++++- .../components/AddOrEditProjectModal.tsx | 71 +++++++++++++------ .../components/AddOrEditScenarioModal.tsx | 17 +++-- front/src/modules/scenario/helpers/utils.ts | 11 +++ .../ManchetteWithSpaceTimeChart.tsx | 39 +++++----- .../SpaceTimeChart/ManchetteMenuButton.tsx | 9 ++- .../SpaceTimeChart/WaypointsModal.tsx | 57 ++++++++++++--- .../useGetProjectedTrainOperationalPoints.ts | 26 ++++++- front/src/modules/simulationResult/types.ts | 9 +++ .../study/components/AddOrEditStudyModal.tsx | 19 +++-- 13 files changed, 290 insertions(+), 94 deletions(-) diff --git a/front/src/applications/operationalStudies/Home.tsx b/front/src/applications/operationalStudies/Home.tsx index 4063d9b7192..f7c81ed150b 100644 --- a/front/src/applications/operationalStudies/Home.tsx +++ b/front/src/applications/operationalStudies/Home.tsx @@ -16,6 +16,7 @@ import { Spinner } from 'common/Loaders'; import SelectionToolbar from 'common/SelectionToolbar'; import ProjectCard from 'modules/project/components/ProjectCard'; import ProjectCardEmpty from 'modules/project/components/ProjectCardEmpty'; +import { cleanScenarioLocalStorage } from 'modules/scenario/helpers/utils'; import { getUserSafeWord } from 'reducers/user/userSelectors'; import { getLogo } from 'utils/logo'; @@ -29,7 +30,7 @@ type SortOptions = | 'LastModifiedAsc' | 'LastModifiedDesc'; -export default function HomeOperationalStudies() { +const HomeOperationalStudies = () => { const { t } = useTranslation('operationalStudies/home'); const safeWord = useSelector(getUserSafeWord); const [sortOption, setSortOption] = useState('LastModifiedDesc'); @@ -37,6 +38,10 @@ export default function HomeOperationalStudies() { const [filterChips, setFilterChips] = useState(''); const [deleteProject] = osrdEditoastApi.endpoints.deleteProjectsByProjectId.useMutation(); + const [getStudies] = osrdEditoastApi.endpoints.getProjectsByProjectIdStudies.useLazyQuery(); + const [getScenarios] = + osrdEditoastApi.endpoints.getProjectsByProjectIdStudiesAndStudyIdScenarios.useLazyQuery(); + const { selectedItemIds: selectedProjectIds, setSelectedItemIds: setSelectedProjectIds, @@ -44,9 +49,28 @@ export default function HomeOperationalStudies() { setItems: setProjectsList, toggleSelection: toggleProjectSelection, deleteItems, - } = useMultiSelection((projectId) => { + } = useMultiSelection(async (projectId) => { + // For each scenario in the selected projects, clean the local storage if a manchette is saved + const { data: studies } = await getStudies({ projectId }); + if (studies) { + const promisedScenarios = studies.results.map(async (study) => { + const { data } = await getScenarios({ + projectId, + studyId: study.id, + }); + return data?.results; + }); + + const scenarios = await Promise.all(promisedScenarios); + + scenarios.flat().forEach((scenario) => { + if (scenario) cleanScenarioLocalStorage(scenario.timetable_id); + }); + } + deleteProject({ projectId }); }); + const handleDeleteProjects = () => { if (selectedProjectIds.length > 0) { deleteItems(); @@ -187,4 +211,6 @@ export default function HomeOperationalStudies() { ); -} +}; + +export default HomeOperationalStudies; diff --git a/front/src/applications/operationalStudies/views/Project.tsx b/front/src/applications/operationalStudies/views/Project.tsx index 1c9f4953479..0ab248d40e5 100644 --- a/front/src/applications/operationalStudies/views/Project.tsx +++ b/front/src/applications/operationalStudies/views/Project.tsx @@ -22,6 +22,7 @@ import OptionsSNCF from 'common/BootstrapSNCF/OptionsSNCF'; import { Loader, Spinner } from 'common/Loaders'; import SelectionToolbar from 'common/SelectionToolbar'; import AddOrEditProjectModal from 'modules/project/components/AddOrEditProjectModal'; +import { cleanScenarioLocalStorage } from 'modules/scenario/helpers/utils'; import StudyCard from 'modules/study/components/StudyCard'; import StudyCardEmpty from 'modules/study/components/StudyCardEmpty'; import { budgetFormat } from 'utils/numbers'; @@ -40,7 +41,7 @@ type ProjectParams = { projectId: string; }; -export default function Project() { +const Project = () => { const { t } = useTranslation(['operationalStudies/project']); const { openModal } = useModal(); const [filter, setFilter] = useState(''); @@ -61,6 +62,29 @@ export default function Project() { [urlProjectId] ); + const { + data: project, + isError: isProjectError, + error: projectError, + } = osrdEditoastApi.endpoints.getProjectsByProjectId.useQuery( + { projectId: +projectId! }, + { + skip: !projectId, + } + ); + + const { data: projectStudies } = osrdEditoastApi.endpoints.getProjectsByProjectIdStudies.useQuery( + { + projectId: Number(projectId), + ordering: sortOption, + pageSize: 1000, + }, + { skip: !projectId } + ); + + const [getScenarios] = + osrdEditoastApi.endpoints.getProjectsByProjectIdStudiesAndStudyIdScenarios.useLazyQuery(); + const { selectedItemIds: selectedStudyIds, setSelectedItemIds: setSelectedStudyIds, @@ -68,8 +92,16 @@ export default function Project() { setItems: setStudiesList, toggleSelection: toggleStudySelection, deleteItems, - } = useMultiSelection((studyId) => { + } = useMultiSelection(async (studyId) => { deleteStudy({ projectId: projectId!, studyId }); + + // For each scenario in the selected studies, clean the local storage if a manchette is saved + const { data: scenarios } = await getScenarios({ projectId: projectId!, studyId }); + if (scenarios) { + scenarios.results.forEach((scenario) => { + cleanScenarioLocalStorage(scenario.timetable_id); + }); + } }); const handleDeleteStudy = () => { @@ -88,26 +120,6 @@ export default function Project() { }, ]; - const { - data: project, - isError: isProjectError, - error: projectError, - } = osrdEditoastApi.endpoints.getProjectsByProjectId.useQuery( - { projectId: +projectId! }, - { - skip: !projectId, - } - ); - - const { data: projectStudies } = osrdEditoastApi.endpoints.getProjectsByProjectIdStudies.useQuery( - { - projectId: Number(projectId), - ordering: sortOption, - pageSize: 1000, - }, - { skip: !projectId } - ); - const updateImage = async () => { if (project?.image) { const image = await getDocument(project.image); @@ -226,7 +238,11 @@ export default function Project() { type="button" onClick={() => openModal( - , + , 'xl', 'no-close-modal' ) @@ -330,4 +346,6 @@ export default function Project() { ); -} +}; + +export default Project; diff --git a/front/src/applications/operationalStudies/views/SimulationResults.tsx b/front/src/applications/operationalStudies/views/SimulationResults.tsx index 26ba44aa6ef..a0cfbea092d 100644 --- a/front/src/applications/operationalStudies/views/SimulationResults.tsx +++ b/front/src/applications/operationalStudies/views/SimulationResults.tsx @@ -69,7 +69,11 @@ const SimulationResults = ({ infraId ); - const projectedOperationalPoints = useGetProjectedTrainOperationalPoints( + const { + operationalPoints: projectedOperationalPoints, + filteredOperationalPoints, + setFilteredOperationalPoints, + } = useGetProjectedTrainOperationalPoints( projectionData?.trainSchedule, projectionData?.trainSchedule.id, infraId @@ -153,6 +157,11 @@ const SimulationResults = ({ operationalPoints={projectedOperationalPoints} projectPathTrainResult={projectPathTrainResult} selectedTrainScheduleId={selectedTrainSchedule?.id} + waypointsModalData={{ + filteredOperationalPoints, + setFilteredOperationalPoints, + projectedTrainPath: projectionData.trainSchedule.path, + }} /> diff --git a/front/src/applications/operationalStudies/views/Study.tsx b/front/src/applications/operationalStudies/views/Study.tsx index ae7a300bd05..811484282dc 100644 --- a/front/src/applications/operationalStudies/views/Study.tsx +++ b/front/src/applications/operationalStudies/views/Study.tsx @@ -22,6 +22,7 @@ import { Loader, Spinner } from 'common/Loaders'; import SelectionToolbar from 'common/SelectionToolbar'; import ScenarioCard from 'modules/scenario/components/ScenarioCard'; import ScenarioCardEmpty from 'modules/scenario/components/ScenarioCardEmpty'; +import { cleanScenarioLocalStorage } from 'modules/scenario/helpers/utils'; import AddOrEditStudyModal from 'modules/study/components/AddOrEditStudyModal'; import { budgetFormat } from 'utils/numbers'; @@ -40,7 +41,7 @@ type studyParams = { studyId: string; }; -export default function Study() { +const Study = () => { const { t } = useTranslation(['operationalStudies/study']); const { openModal } = useModal(); const { projectId: urlProjectId, studyId: urlStudyId } = useParams() as studyParams; @@ -98,6 +99,10 @@ export default function Study() { deleteItems, } = useMultiSelection((scenarioId) => { deleteScenario({ projectId: projectId!, studyId: studyId!, scenarioId }); + + // For each scenarios, clean the local storage if a manchette is saved + const deletedScenario = scenarios!.results.find((scenario) => scenario.id === scenarioId); + cleanScenarioLocalStorage(deletedScenario!.timetable_id); }); const handleDeleteScenario = () => { if (selectedScenarioIds.length > 0 && studyId && projectId) { @@ -247,7 +252,11 @@ export default function Study() { type="button" onClick={() => openModal( - , + , 'xl', 'no-close-modal' ) @@ -370,4 +379,6 @@ export default function Study() { ); -} +}; + +export default Study; diff --git a/front/src/modules/project/components/AddOrEditProjectModal.tsx b/front/src/modules/project/components/AddOrEditProjectModal.tsx index 301d5eb5259..ccb33eaf54a 100644 --- a/front/src/modules/project/components/AddOrEditProjectModal.tsx +++ b/front/src/modules/project/components/AddOrEditProjectModal.tsx @@ -14,7 +14,11 @@ import remarkGfm from 'remark-gfm'; import PictureUploader from 'applications/operationalStudies/components/Project/PictureUploader'; import { postDocument } from 'common/api/documentApi'; import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; -import type { ProjectWithStudies, ProjectCreateForm } from 'common/api/osrdEditoastApi'; +import type { + ProjectWithStudies, + ProjectCreateForm, + StudyWithScenarios, +} from 'common/api/osrdEditoastApi'; import ChipsSNCF from 'common/BootstrapSNCF/ChipsSNCF'; import InputSNCF from 'common/BootstrapSNCF/InputSNCF'; import { ConfirmModal } from 'common/BootstrapSNCF/ModalSNCF'; @@ -24,6 +28,7 @@ import ModalHeaderSNCF from 'common/BootstrapSNCF/ModalSNCF/ModalHeaderSNCF'; import { ModalContext } from 'common/BootstrapSNCF/ModalSNCF/ModalProvider'; import TextareaSNCF from 'common/BootstrapSNCF/TextareaSNCF'; import { useOsrdConfActions } from 'common/osrdContext'; +import { cleanScenarioLocalStorage } from 'modules/scenario/helpers/utils'; import { setFailure, setSuccess } from 'reducers/main'; import { getUserSafeWord } from 'reducers/user/userSelectors'; import { useAppDispatch } from 'store'; @@ -53,12 +58,14 @@ type AddOrEditProjectModalProps = { editionMode?: boolean; project?: ProjectWithStudies; getProject?: (v: boolean) => void; + projectStudies?: StudyWithScenarios[]; }; export default function AddOrEditProjectModal({ editionMode, project, getProject, + projectStudies, }: AddOrEditProjectModalProps) { const { t } = useTranslation(['operationalStudies/project', 'translation']); const { closeModal, isOpen } = useContext(ModalContext); @@ -70,6 +77,8 @@ export default function AddOrEditProjectModal({ const navigate = useNavigate(); const safeWord = useSelector(getUserSafeWord); + const [getScenarios] = + osrdEditoastApi.endpoints.getProjectsByProjectIdStudiesAndStudyIdScenarios.useLazyQuery(); const [postProject] = osrdEditoastApi.endpoints.postProjects.useMutation(); const [patchProject] = osrdEditoastApi.endpoints.patchProjectsByProjectId.useMutation(); const [deleteProject] = osrdEditoastApi.endpoints.deleteProjectsByProjectId.useMutation(); @@ -204,30 +213,46 @@ export default function AddOrEditProjectModal({ }; const removeProject = async () => { - if (project) { - await deleteProject({ projectId: project.id }) - .unwrap() - .then(() => { - dispatch(updateProjectID(undefined)); - navigate(`/operational-studies/projects/`); - closeModal(); - dispatch( - setSuccess({ - title: t('projectDeleted'), - text: t('projectDeletedDetails', { name: project.name }), - }) - ); - }) - .catch((e) => { - dispatch( - setFailure( - castErrorToFailure(e, { - name: t('error.unableToDeleteProject'), - }) - ) - ); + if (projectStudies) { + // For each scenario in the project, clean the local storage if a manchette is saved + const promisedScenarios = projectStudies.map(async (study) => { + const { data } = await getScenarios({ + projectId: project!.id, + studyId: study.id, }); + return data?.results; + }); + + const scenarios = await Promise.all(promisedScenarios); + + scenarios.flat().forEach((scenario) => { + if (scenario) cleanScenarioLocalStorage(scenario.timetable_id); + }); } + + await deleteProject({ projectId: project!.id }) + .unwrap() + .then(async () => { + dispatch(updateProjectID(undefined)); + + navigate(`/operational-studies/projects/`); + closeModal(); + dispatch( + setSuccess({ + title: t('projectDeleted'), + text: t('projectDeletedDetails', { name: project!.name }), + }) + ); + }) + .catch((e) => { + dispatch( + setFailure( + castErrorToFailure(e, { + name: t('error.unableToDeleteProject'), + }) + ) + ); + }); }; const debouncedObjectives = useDebounce(currentProject.objectives, 500); diff --git a/front/src/modules/scenario/components/AddOrEditScenarioModal.tsx b/front/src/modules/scenario/components/AddOrEditScenarioModal.tsx index 63e7442795a..5239fd77415 100644 --- a/front/src/modules/scenario/components/AddOrEditScenarioModal.tsx +++ b/front/src/modules/scenario/components/AddOrEditScenarioModal.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'; import { FaPlus } from 'react-icons/fa'; import { GiElectric } from 'react-icons/gi'; import { MdDescription, MdTitle } from 'react-icons/md'; +import { useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import { osrdEditoastApi, type ScenarioPatchForm } from 'common/api/osrdEditoastApi'; @@ -19,7 +20,7 @@ import ModalHeaderSNCF from 'common/BootstrapSNCF/ModalSNCF/ModalHeaderSNCF'; import { ModalContext } from 'common/BootstrapSNCF/ModalSNCF/ModalProvider'; import SelectImprovedSNCF from 'common/BootstrapSNCF/SelectImprovedSNCF'; import TextareaSNCF from 'common/BootstrapSNCF/TextareaSNCF'; -import { useInfraID, useOsrdConfActions } from 'common/osrdContext'; +import { useInfraID, useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; import { InfraSelectorModal } from 'modules/infra/components/InfraSelector'; import { setFailure, setSuccess } from 'reducers/main'; import { useAppDispatch } from 'store'; @@ -28,7 +29,7 @@ import useInputChange from 'utils/hooks/useInputChange'; import useModalFocusTrap from 'utils/hooks/useModalFocusTrap'; import useOutsideClick from 'utils/hooks/useOutsideClick'; -import { checkScenarioFields } from '../helpers/utils'; +import { checkScenarioFields, cleanScenarioLocalStorage } from '../helpers/utils'; // TODO: use ScenarioCreateForm from osrdEditoastApi to harmonize with study and project // and then change checkNameInvalidity @@ -57,15 +58,14 @@ const emptyScenario: ScenarioForm = { tags: [], }; -export default function AddOrEditScenarioModal({ - editionMode = false, - scenario, -}: AddOrEditScenarioModalProps) { +const AddOrEditScenarioModal = ({ editionMode = false, scenario }: AddOrEditScenarioModalProps) => { const { t } = useTranslation(['operationalStudies/scenario', 'translation']); const { closeModal, isOpen } = useContext(ModalContext); const dispatch = useAppDispatch(); const navigate = useNavigate(); const infraID = useInfraID(); + const { getTimetableID } = useOsrdConfSelectors(); + const timetableId = useSelector(getTimetableID); const { updateScenarioID } = useOsrdConfActions(); const [currentScenario, setCurrentScenario] = useState(scenario || emptyScenario); @@ -222,6 +222,7 @@ export default function AddOrEditScenarioModal({ .unwrap() .then(() => { dispatch(updateScenarioID(undefined)); + cleanScenarioLocalStorage(timetableId!); navigate(`projects/${projectId}/studies/${studyId}`); closeModal(); dispatch( @@ -411,4 +412,6 @@ export default function AddOrEditScenarioModal({ ); -} +}; + +export default AddOrEditScenarioModal; diff --git a/front/src/modules/scenario/helpers/utils.ts b/front/src/modules/scenario/helpers/utils.ts index 772a2ed51e3..d83a9367f97 100644 --- a/front/src/modules/scenario/helpers/utils.ts +++ b/front/src/modules/scenario/helpers/utils.ts @@ -30,3 +30,14 @@ export const checkScenarioFields = ( name: isInvalidName(scenario.name), description: isInvalidString(SMALL_TEXT_AREA_MAX_LENGTH, scenario.description), }); + +/** + * Clean the local storage for potential store manchette + */ +export const cleanScenarioLocalStorage = (timetableId: number) => { + Object.keys(localStorage).forEach((key) => { + if (key.startsWith(timetableId.toString())) { + localStorage.removeItem(key); + } + }); +}; diff --git a/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx b/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx index 18342670032..a6d72077df3 100644 --- a/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx +++ b/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx @@ -2,23 +2,20 @@ import { useRef, useState } from 'react'; import { Manchette } from '@osrd-project/ui-manchette'; import { useManchettesWithSpaceTimeChart } from '@osrd-project/ui-manchette-with-spacetimechart'; -import { SpaceTimeChart, PathLayer } from '@osrd-project/ui-spacetimechart'; +import { PathLayer, SpaceTimeChart } from '@osrd-project/ui-spacetimechart'; import type { TrainSpaceTimeData } from 'applications/operationalStudies/types'; -import type { OperationalPointExtensions, OperationalPointPart } from 'common/api/osrdEditoastApi'; +import type { PathProperties } from 'common/api/osrdEditoastApi'; +import type { WaypointsModalData } from 'modules/simulationResult/types'; -import WaypointsModal from '../SpaceTimeChart/WaypointsModal'; import ManchetteMenuButton from '../SpaceTimeChart/ManchetteMenuButton'; +import WaypointsModal from '../SpaceTimeChart/WaypointsModal'; type ManchetteWithSpaceTimeChartProps = { - operationalPoints: { - extensions?: OperationalPointExtensions; - id: string; - part: OperationalPointPart; - position: number; - }[]; + operationalPoints: NonNullable; projectPathTrainResult: TrainSpaceTimeData[]; selectedTrainScheduleId?: number; + waypointsModalData?: WaypointsModalData; }; const DEFAULT_HEIGHT = 561; @@ -26,6 +23,7 @@ const ManchetteWithSpaceTimeChartWrapper = ({ operationalPoints, projectPathTrainResult, selectedTrainScheduleId, + waypointsModalData, }: ManchetteWithSpaceTimeChartProps) => { const [heightOfManchetteWithSpaceTimeChart] = useState(DEFAULT_HEIGHT); const manchetteWithSpaceTimeChartRef = useRef(null); @@ -34,7 +32,7 @@ const ManchetteWithSpaceTimeChartWrapper = ({ const [waypointsModalOpen, setWaypointsModalOpen] = useState(false); const { manchetteProps, spaceTimeChartProps, handleScroll } = useManchettesWithSpaceTimeChart( - operationalPoints, + waypointsModalData?.filteredOperationalPoints ?? operationalPoints, projectPathTrainResult, manchetteWithSpaceTimeChartRef, selectedTrainScheduleId @@ -43,12 +41,17 @@ const ManchetteWithSpaceTimeChartWrapper = ({ return (
- - + + {waypointsModalOpen && waypointsModalData && ( + + )}
+new Date(p.departure_time)))} {...spaceTimeChartProps} > diff --git a/front/src/modules/simulationResult/components/SpaceTimeChart/ManchetteMenuButton.tsx b/front/src/modules/simulationResult/components/SpaceTimeChart/ManchetteMenuButton.tsx index 03336def429..983841a63de 100644 --- a/front/src/modules/simulationResult/components/SpaceTimeChart/ManchetteMenuButton.tsx +++ b/front/src/modules/simulationResult/components/SpaceTimeChart/ManchetteMenuButton.tsx @@ -8,7 +8,11 @@ import type { OSRDMenuItem } from 'common/OSRDMenu'; import OSRDMenu from 'common/OSRDMenu'; import useModalFocusTrap from 'utils/hooks/useModalFocusTrap'; -const ManchetteMenuButton = () => { +type ManchetteMenuButtonProps = { + setWaypointsModalOpen: (waypointsModalOpen: boolean) => void; +}; + +const ManchetteMenuButton = ({ setWaypointsModalOpen }: ManchetteMenuButtonProps) => { const { t } = useTranslation('simulation'); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -22,7 +26,8 @@ const ManchetteMenuButton = () => { title: t('manchetteSettings.waypointsVisibility'), icon: , onClick: () => { - closeMenu(); // TODO : in #8628, change this to open the waypoints modal + closeMenu(); + setWaypointsModalOpen(true); }, }, ]; diff --git a/front/src/modules/simulationResult/components/SpaceTimeChart/WaypointsModal.tsx b/front/src/modules/simulationResult/components/SpaceTimeChart/WaypointsModal.tsx index 72f76c6a5e2..5c332d159f0 100644 --- a/front/src/modules/simulationResult/components/SpaceTimeChart/WaypointsModal.tsx +++ b/front/src/modules/simulationResult/components/SpaceTimeChart/WaypointsModal.tsx @@ -3,9 +3,12 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Button, Checkbox } from '@osrd-project/ui-core'; import { Alert } from '@osrd-project/ui-icons'; import cx from 'classnames'; +import { omit } from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; -import type { PathProperties } from 'common/api/osrdEditoastApi'; +import type { PathProperties, TrainScheduleBase } from 'common/api/osrdEditoastApi'; +import { useOsrdConfSelectors } from 'common/osrdContext'; import useModalFocusTrap from 'utils/hooks/useModalFocusTrap'; import { mmToKm } from 'utils/physics'; @@ -13,18 +16,27 @@ type WaypointsModalProps = { waypointsModalOpen: boolean; setWaypointsModalOpen: (open: boolean) => void; waypoints: NonNullable; + filteredWaypoints: NonNullable; + setFilteredWaypoints: (waypoints: NonNullable) => void; + projectedTrainPath: TrainScheduleBase['path']; }; const WaypointsModal = ({ waypointsModalOpen, setWaypointsModalOpen, waypoints, + filteredWaypoints, + setFilteredWaypoints, + projectedTrainPath, }: WaypointsModalProps) => { const { t } = useTranslation(); + const { getTimetableID } = useOsrdConfSelectors(); + const timetableId = useSelector(getTimetableID); + const modalRef = useRef(null); const [selectedWaypoints, setSelectedWaypoints] = useState>( - new Set(waypoints.map((_, i) => i)) + new Set(filteredWaypoints.map((_, i) => i)) ); const [lastSelectedIndex, setLastSelectedIndex] = useState(null); const [isNotEnoughSelectedWaypoints, setIsNotEnoughSelectedWaypoints] = useState(false); @@ -39,9 +51,14 @@ const WaypointsModal = ({ [selectedWaypoints, allWaypointsSelected] ); - // TODO (#8628): handle checking local storage to see if a key matches the current manchette const openModal = () => { modalRef.current?.showModal(); + const filteredWaypointsIds = filteredWaypoints.map((waypoint) => waypoint.id); + const filteredWaypointsIndexes = waypoints + .map((waypoint, index) => (filteredWaypointsIds.includes(waypoint.id) ? index : -1)) + .filter((index) => index !== -1); + + setSelectedWaypoints(new Set(filteredWaypointsIndexes)); }; const closeModal = () => { @@ -51,12 +68,24 @@ const WaypointsModal = ({ useModalFocusTrap(modalRef, closeModal); - // TODO (#8628): handle saving the manchette in local storage to keep the current selected waypoints - const submit = () => { + const handleSubmit = () => { if (selectedWaypoints.size < 2) { setIsNotEnoughSelectedWaypoints(true); return; } + const newFilteredWaypoints = waypoints.filter((_, i) => selectedWaypoints.has(i)); + setFilteredWaypoints(newFilteredWaypoints); + + // We need to removed the id because it can change for waypoints added by map click + const simplifiedPath = projectedTrainPath.map((waypoint) => omit(waypoint, ['id', 'deleted'])); + + // TODO : when switching to the manchette back-end manager, remove all logic using + // cleanScenarioLocalStorage from projet/study/scenario components (single/multi select) + localStorage.setItem( + `${timetableId}-${JSON.stringify(simplifiedPath)}`, + JSON.stringify(newFilteredWaypoints) + ); + closeModal(); }; @@ -73,8 +102,17 @@ const WaypointsModal = ({ const minIndex = Math.min(index, lastSelectedIndex); const maxIndex = Math.max(index, lastSelectedIndex); const newSet = new Set(selectedWaypoints); - for (let i = minIndex; i <= maxIndex; i += 1) { - newSet.add(i); + + // If the lastSelectedIndex has been checked, we check all waypoints of the range + // If it has been unchecked, we uncheck all waypoints of the range + if (newSet.has(lastSelectedIndex)) { + for (let i = minIndex; i <= maxIndex; i += 1) { + newSet.add(i); + } + } else { + for (let i = minIndex; i <= maxIndex; i += 1) { + newSet.delete(i); + } } setSelectedWaypoints(newSet); } else { @@ -120,6 +158,9 @@ const WaypointsModal = ({ {}} onClick={(e) => { handleWaypointClick(e, index); }} @@ -141,7 +182,7 @@ const WaypointsModal = ({ })} >