From 16ce34125a76cba159557d712a71f83850275d43 Mon Sep 17 00:00:00 2001 From: Alice Khoudli Date: Wed, 25 Feb 2026 00:53:32 +0100 Subject: [PATCH 1/4] wip nge update events trinrun type Signed-off-by: Alice Khoudli --- front/package-lock.json | 8 ++-- front/package.json | 2 +- .../MacroEditor/ngeToOsrd/ngeToOsrd.ts | 3 +- .../MacroEditor/ngeToOsrd/trainrun.ts | 18 ++++---- .../views/Scenario/components/NGE/types.ts | 43 ++++++++++++++++--- 5 files changed, 50 insertions(+), 24 deletions(-) diff --git a/front/package-lock.json b/front/package-lock.json index 2a16a464a46..bb97be5117a 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -17,7 +17,7 @@ "@ag-media/react-pdf-table": "^2.0.3", "@nivo/core": "^0.99.0", "@nivo/line": "^0.99.0", - "@osrd-project/netzgrafik-frontend": "0.0.0-snapshot.5eafed2a3fd1e8818d84679046e2c2274a948bc1", + "@osrd-project/netzgrafik-frontend": "0.0.0-snapshot.09bd436ff2dd9d6422a08dc05f9a551c7382e642", "@osrd-project/ui-charts": "0.0.1-dev", "@osrd-project/ui-core": "0.0.1-dev", "@osrd-project/ui-icons": "0.0.1-dev", @@ -3444,9 +3444,9 @@ "link": true }, "node_modules/@osrd-project/netzgrafik-frontend": { - "version": "0.0.0-snapshot.5eafed2a3fd1e8818d84679046e2c2274a948bc1", - "resolved": "https://registry.npmjs.org/@osrd-project/netzgrafik-frontend/-/netzgrafik-frontend-0.0.0-snapshot.5eafed2a3fd1e8818d84679046e2c2274a948bc1.tgz", - "integrity": "sha512-j9+TYrlcW6sdJdsoK43X0nEIeu+c9/qe6SnacHGgVT0fG8Vez9otKLkLR8fzUpTpMZaAoAuXK+rGJuXysUgNAw==" + "version": "0.0.0-snapshot.09bd436ff2dd9d6422a08dc05f9a551c7382e642", + "resolved": "https://registry.npmjs.org/@osrd-project/netzgrafik-frontend/-/netzgrafik-frontend-0.0.0-snapshot.09bd436ff2dd9d6422a08dc05f9a551c7382e642.tgz", + "integrity": "sha512-T87X6n5+nw1X3ehW8YPjiaXetoBWgfXDbsmid6JzDD/T7tNd6HJJ/VKZSzau1qMwuWnbQeN5oL1HTY3D8J2Ojg==" }, "node_modules/@osrd-project/storybook": { "resolved": "ui/storybook", diff --git a/front/package.json b/front/package.json index 852d82c295e..171aa88ccb0 100644 --- a/front/package.json +++ b/front/package.json @@ -14,7 +14,7 @@ "@ag-media/react-pdf-table": "^2.0.3", "@nivo/core": "^0.99.0", "@nivo/line": "^0.99.0", - "@osrd-project/netzgrafik-frontend": "0.0.0-snapshot.5eafed2a3fd1e8818d84679046e2c2274a948bc1", + "@osrd-project/netzgrafik-frontend": "0.0.0-snapshot.09bd436ff2dd9d6422a08dc05f9a551c7382e642", "@osrd-project/ui-charts": "0.0.1-dev", "@osrd-project/ui-core": "0.0.1-dev", "@osrd-project/ui-icons": "0.0.1-dev", diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts index cfe642996af..ff65314fae6 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts @@ -104,9 +104,8 @@ export const handleOperation = async ({ break; case 'trainrun': { await handleTrainrunOperation({ - type, netzgrafikDto, - trainrunId: event.trainrun.id, + trainrunEvent: event, trainScheduleSetId, infraId, state, diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts index 2d715f70ecb..f8d5ec4e74d 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts @@ -19,11 +19,11 @@ import { isPacedTrainId } from 'utils/trainId'; import { checkChangeGroups } from '../../ManageTimetableItem/helpers/buildPacedTrainException'; import type { NetzgrafikDto, - NGEEvent, TrainrunSectionDto, NodeDto, TimeLockDto, TrainrunDto, + NGETrainrunEvent, } from '../../NGE/types'; import { DEFAULT_TRAIN_SCHEDULE_PAYLOAD, @@ -646,9 +646,8 @@ export const handleUpdateTimetableItem = async ({ }; export const handleTrainrunOperation = async ({ - type, netzgrafikDto, - trainrunId, + trainrunEvent, trainScheduleSetId, infraId, state, @@ -656,9 +655,8 @@ export const handleTrainrunOperation = async ({ addUpsertedTimetableItems, addDeletedTimetableItemIds, }: { - type: NGEEvent['type']; netzgrafikDto: NetzgrafikDto; - trainrunId: number; + trainrunEvent: NGETrainrunEvent; trainScheduleSetId: number; infraId: number; state: MacroEditorState; @@ -666,12 +664,12 @@ export const handleTrainrunOperation = async ({ addUpsertedTimetableItems: (timetableItems: TimetableItem[]) => void; addDeletedTimetableItemIds: (timetableItemIds: TimetableItemId[]) => void; }) => { - const trainrun = netzgrafikDto.trainruns.find((tr) => tr.id === trainrunId); - switch (type) { + const trainrun = trainrunEvent.trainrun; + switch (trainrunEvent.type) { case 'create': { await handleCreateTimetableItem( netzgrafikDto, - trainrun!, + trainrun, trainScheduleSetId, infraId, state, @@ -683,7 +681,7 @@ export const handleTrainrunOperation = async ({ case 'update': { await handleUpdateTimetableItem({ netzgrafikDto, - trainrun: trainrun!, + trainrun, trainScheduleSetId, infraId, dispatch, @@ -694,7 +692,7 @@ export const handleTrainrunOperation = async ({ break; } case 'delete': { - await handleDeleteTimetableItem(trainrunId, state, dispatch, addDeletedTimetableItemIds); + await handleDeleteTimetableItem(trainrun.id, state, dispatch, addDeletedTimetableItemIds); break; } default: diff --git a/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts b/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts index 27afa945204..79d4af070b5 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts @@ -220,17 +220,46 @@ export type NetzgrafikDto = { }; }; -export type NGEEvent = { - type: 'create' | 'delete' | 'update'; -} & ( +type TrainrunUpdateTag = + | 'nodes' + | 'times' + | 'numberOfStops' + | 'name' + | 'categoryId' + | 'frequencyId' + | 'timeCategoryId' + | 'labelIds' + | 'direction'; + +export type NGETrainrunEvent = + | { + type: 'create'; + objectType: 'trainrun'; + trainrun: TrainrunDto; + duplicatedTrainrunId?: number; + } | { + type: 'update'; objectType: 'trainrun'; trainrun: TrainrunDto; + tags: TrainrunUpdateTag[]; + oneWayDirection?: 'forward' | 'backward'; } - | { objectType: 'node'; node: NodeDto } - | { objectType: 'label'; label: LabelDto } - | { objectType: 'note'; note: FreeFloatingTextDto } -); + | { + type: 'delete'; + objectType: 'trainrun'; + trainrun: TrainrunDto; + }; + +export type NGEEvent = + | NGETrainrunEvent + | ({ + type: 'create' | 'delete' | 'update'; + } & ( + | { objectType: 'node'; node: NodeDto } + | { objectType: 'label'; label: LabelDto } + | { objectType: 'note'; note: FreeFloatingTextDto } + )); export type LabelDto = { id: number | string; From 8b90e39cc7d3ce1c5ccf8bf22b3cab20bd8aed70 Mon Sep 17 00:00:00 2001 From: Alice Khoudli Date: Wed, 25 Feb 2026 18:06:17 +0100 Subject: [PATCH 2/4] front: ngeToOsrd: handle duplicate train event Signed-off-by: Alice Khoudli --- .../MacroEditor/ngeToOsrd/trainrun.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts index f8d5ec4e74d..c5f44ee65d7 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts @@ -388,12 +388,25 @@ export const createPacedAttributesFromTrainrun = ( const handleCreateTimetableItem = async ( netzgrafikDto: NetzgrafikDto, trainrun: TrainrunDto, + duplicatedTrainrunId: number | undefined, trainScheduleSetId: number, infraId: number, state: MacroEditorState, dispatch: AppDispatch, addUpsertedTimetableItems: (timetableItems: TimetableItem[]) => void ) => { + const duplicatedTimetableItemIds = duplicatedTrainrunId + ? state.timetableItemIdByNgeId.get(duplicatedTrainrunId) + : undefined; + const duplicatedForwardTimetableItem = duplicatedTimetableItemIds?.[0] + ? await fetchTimetableItem(duplicatedTimetableItemIds[0], dispatch) + : { id: undefined, train_name: undefined }; + const duplicatedReturnTimetableItem = duplicatedTimetableItemIds?.[1] + ? await fetchTimetableItem(duplicatedTimetableItemIds[1], dispatch) + : { id: undefined, train_name: undefined }; + const { id: _id, train_name: _name, ...duplicatedForwardBase } = duplicatedForwardTimetableItem; + const { id: __id, train_name: __name, ...duplicatedReturnBase } = duplicatedReturnTimetableItem; + const trainrunSections = getContinuousTrainrunSectionsByTrainrunId(netzgrafikDto, trainrun.id); const labels = getTrainrunLabels(netzgrafikDto, trainrun); @@ -431,11 +444,14 @@ const handleCreateTimetableItem = async ( train_name: trainrun.name, labels, category, + ...duplicatedForwardBase, ...pathAndSchedule, }; const returnTrip = - trainrun.direction === 'round_trip' ? { ...forwardTrip, ...returnPathAndSchedule } : undefined; + trainrun.direction === 'round_trip' + ? { ...forwardTrip, ...duplicatedReturnBase, ...returnPathAndSchedule } + : undefined; const timetableItemsToCreate = returnTrip ? [forwardTrip, returnTrip] : [forwardTrip]; @@ -670,6 +686,7 @@ export const handleTrainrunOperation = async ({ await handleCreateTimetableItem( netzgrafikDto, trainrun, + trainrunEvent.duplicatedTrainrunId, trainScheduleSetId, infraId, state, From 1d6f6a0fc5fd62674c0d9efe6ac2f1f7870abf67 Mon Sep 17 00:00:00 2001 From: Alice Khoudli Date: Thu, 26 Feb 2026 14:42:04 +0100 Subject: [PATCH 3/4] front: ngeToOsrd: partially update trains using tags Signed-off-by: Alice Khoudli --- .../MacroEditor/ngeToOsrd/ngeToOsrd.ts | 1 + .../MacroEditor/ngeToOsrd/trainrun.ts | 47 +++++++++++-------- .../views/Scenario/components/NGE/types.ts | 2 +- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts index ff65314fae6..10d46d9b9d7 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/ngeToOsrd.ts @@ -52,6 +52,7 @@ const handleLabelOperation = async ({ await handleUpdateTimetableItem({ netzgrafikDto, trainrun, + tags: ['labelIds'], trainScheduleSetId, infraId, state, diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts index c5f44ee65d7..0a31e918eed 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts @@ -24,6 +24,7 @@ import type { TimeLockDto, TrainrunDto, NGETrainrunEvent, + TrainrunUpdateTag, } from '../../NGE/types'; import { DEFAULT_TRAIN_SCHEDULE_PAYLOAD, @@ -510,6 +511,7 @@ const handleDeleteTimetableItem = async ( export const handleUpdateTimetableItem = async ({ netzgrafikDto, trainrun, + tags, trainScheduleSetId, infraId, state, @@ -519,6 +521,7 @@ export const handleUpdateTimetableItem = async ({ }: { netzgrafikDto: NetzgrafikDto; trainrun: TrainrunDto; + tags: TrainrunUpdateTag[]; infraId: number; trainScheduleSetId: number; state: MacroEditorState; @@ -530,14 +533,14 @@ export const handleUpdateTimetableItem = async ({ const oldForwardTimetableItem = await fetchTimetableItem(timetableItemIds[0], dispatch); const trainrunSections = getContinuousTrainrunSectionsByTrainrunId(netzgrafikDto, trainrun.id); const labels = getTrainrunLabels(netzgrafikDto, trainrun); - const forwardPathAndSchedule = generatePathAndSchedule( + const { path: forwardPath, ...forwardSchedule } = generatePathAndSchedule( trainrunSections, netzgrafikDto.nodes, new Date(oldForwardTimetableItem.start_time), TRAINRUN_DIRECTIONS.FORWARD, state ); - await populateSecondaryCodesInPath(forwardPathAndSchedule.path, infraId, dispatch); + await populateSecondaryCodesInPath(forwardPath, infraId, dispatch); const { id: _id, ...timetableItemBase } = oldForwardTimetableItem; @@ -550,13 +553,13 @@ export const handleUpdateTimetableItem = async ({ const newForwardTrainBase: Omit = { ...timetableItemBase, - train_name: trainrun.name, - labels, - // Reset margins because they contain references to path items - margins: undefined, - paced, - category, - ...forwardPathAndSchedule, + ...(tags.includes('name') && { train_name: trainrun.name }), + ...(tags.includes('labelIds') && { labels }), + // Reset margins if the path changed because they contain references to path items + ...(tags.includes('nodes') && { path: forwardPath, margins: undefined, ...forwardSchedule }), + ...(tags.includes('times') && { ...forwardSchedule }), + ...(tags.includes('frequencyId') && { paced }), + ...(tags.includes('categoryId') && { category }), }; if (paced && oldForwardTimetableItem.paced) { @@ -588,7 +591,7 @@ export const handleUpdateTimetableItem = async ({ return; } - const returnPathAndSchedule = generatePathAndSchedule( + const { path: returnPath, ...returnSchedule } = generatePathAndSchedule( trainrunSections, netzgrafikDto.nodes, new Date(oldForwardTimetableItem.start_time), @@ -596,7 +599,7 @@ export const handleUpdateTimetableItem = async ({ state ); - await populateSecondaryCodesInPath(returnPathAndSchedule.path, infraId, dispatch); + await populateSecondaryCodesInPath(returnPath, infraId, dispatch); let newReturnTimetableItem: TimetableItem; const returnPaced: TrainSchedule['paced'] = paced ? { ...paced, exceptions: [] } : null; @@ -607,13 +610,16 @@ export const handleUpdateTimetableItem = async ({ const { id: _return_id, ...oldReturnTrainBase } = oldReturnTimetableItem; const newReturnTrainBase: Omit = { ...oldReturnTrainBase, - train_name: trainrun.name, - labels, - // Reset margins because they contain references to path items - margins: undefined, - paced: returnPaced, - category, - ...returnPathAndSchedule, + ...(tags.includes('name') && { train_name: trainrun.name }), + ...(tags.includes('labelIds') && { labels }), + // Reset margins if the path changed because they contain references to path items + ...(tags.includes('nodes') && { path: returnPath, margins: undefined, ...returnSchedule }), + ...(tags.includes('times') && { + schedule: returnSchedule.schedule, + start_time: returnSchedule.start_time, + }), + ...(tags.includes('frequencyId') && { paced: returnPaced }), + ...(tags.includes('categoryId') && { category }), }; if (returnPaced && oldReturnTimetableItem.paced) { @@ -641,7 +647,8 @@ export const handleUpdateTimetableItem = async ({ const returnPacedTrain: TrainSchedule = { ...pacedTrainWithoutTrainScheduleSetId, - ...returnPathAndSchedule, + ...returnSchedule, + path: returnPath, paced: returnPaced, }; @@ -699,6 +706,7 @@ export const handleTrainrunOperation = async ({ await handleUpdateTimetableItem({ netzgrafikDto, trainrun, + tags: trainrunEvent.tags, trainScheduleSetId, infraId, dispatch, @@ -753,6 +761,7 @@ export const updateTrainrunsByNode = async ({ await handleUpdateTimetableItem({ netzgrafikDto, trainrun, + tags: ['nodes'], trainScheduleSetId, infraId, dispatch, diff --git a/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts b/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts index 79d4af070b5..6acb10fc66e 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/NGE/types.ts @@ -220,7 +220,7 @@ export type NetzgrafikDto = { }; }; -type TrainrunUpdateTag = +export type TrainrunUpdateTag = | 'nodes' | 'times' | 'numberOfStops' From aca239d0dda635381a6d1c755d7377e1fcd8edd1 Mon Sep 17 00:00:00 2001 From: Alice Khoudli Date: Fri, 27 Feb 2026 16:00:43 +0100 Subject: [PATCH 4/4] front: ngeToOsrd: use onewaydirection in train update Signed-off-by: Alice Khoudli --- .../MacroEditor/ngeToOsrd/trainrun.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts index 0a31e918eed..db6405629b5 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd/trainrun.ts @@ -512,6 +512,7 @@ export const handleUpdateTimetableItem = async ({ netzgrafikDto, trainrun, tags, + oneWayDirection, trainScheduleSetId, infraId, state, @@ -522,6 +523,7 @@ export const handleUpdateTimetableItem = async ({ netzgrafikDto: NetzgrafikDto; trainrun: TrainrunDto; tags: TrainrunUpdateTag[]; + oneWayDirection?: 'forward' | 'backward'; infraId: number; trainScheduleSetId: number; state: MacroEditorState; @@ -530,7 +532,14 @@ export const handleUpdateTimetableItem = async ({ addDeletedTimetableItemIds: (timetableItemIds: TimetableItemId[]) => void; }) => { const timetableItemIds = state.timetableItemIdByNgeId.get(trainrun.id)!; - const oldForwardTimetableItem = await fetchTimetableItem(timetableItemIds[0], dispatch); + let oldForwardId = timetableItemIds[0]; + if (oneWayDirection === 'backward') { + // Case 1: Switching from round trip to the return trip (now forward) + if (timetableItemIds[1]) oldForwardId = timetableItemIds[1]; + // Case 2: Inverting the direction of a one way train (we sadly don't store the old return) + else tags.push('nodes', 'times'); + } + const oldForwardTimetableItem = await fetchTimetableItem(oldForwardId, dispatch); const trainrunSections = getContinuousTrainrunSectionsByTrainrunId(netzgrafikDto, trainrun.id); const labels = getTrainrunLabels(netzgrafikDto, trainrun); const { path: forwardPath, ...forwardSchedule } = generatePathAndSchedule( @@ -581,10 +590,10 @@ export const handleUpdateTimetableItem = async ({ if (trainrun.direction === 'one_way') { if (timetableItemIds[1]) { - // NGE always selects the forward trip by default when going from round trip to one way trip, - // thus the trip that needs to be deleted is always the return trip await storeRoundTrip(dispatch, newForwardTimetableItem.id); - await deleteTimetableItemById(timetableItemIds[1], dispatch, addDeletedTimetableItemIds); + const oldReturnId = + oneWayDirection !== 'backward' ? timetableItemIds[1] : timetableItemIds[0]; + await deleteTimetableItemById(oldReturnId, dispatch, addDeletedTimetableItemIds); } state.timetableItemIdByNgeId.set(trainrun.id, [newForwardTimetableItem.id, null]); @@ -707,6 +716,7 @@ export const handleTrainrunOperation = async ({ netzgrafikDto, trainrun, tags: trainrunEvent.tags, + oneWayDirection: trainrunEvent.oneWayDirection, trainScheduleSetId, infraId, dispatch,