Skip to content

Commit c6a6bf3

Browse files
committed
front: use Date instead of string in OsrdConfState.startTime
Improves type safety. Previously, it was important for OsrdConfState.startTime to be in a specific timezone, because the string was later truncated and directly used on an datetime-local form input. This patch improves robustness by converting to the local timezone right before feeding the value to the datetime-local form input, making it so a different timezone in OsrdConfState.startTime doesn't break the app. IsoDateTimeString is not longer used and can be dropped. Signed-off-by: Simon Ser <[email protected]>
1 parent 0b995f1 commit c6a6bf3

File tree

13 files changed

+41
-52
lines changed

13 files changed

+41
-52
lines changed

front/src/common/types.ts

-9
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@ export const DATA_TYPES = {
1515
*/
1616
export type TimeString = string;
1717

18-
/**
19-
* A string with the complete iso format
20-
*
21-
* @example "2024-08-08T10:12:46.209Z"
22-
* @example "2024-08-08T10:12:46Z"
23-
* @example "2024-08-08T10:12:46+02:00"
24-
*/
25-
export type IsoDateTimeString = string;
26-
2718
/**
2819
* A ISO 8601 duration string
2920
* @example "PT3600S"

front/src/modules/trainschedule/components/ManageTrainSchedule/AddTrainScheduleButton.tsx

+3-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useStoreDataForRollingStockSelector } from 'modules/rollingStock/compon
1313
import trainNameWithNum from 'modules/trainschedule/components/ManageTrainSchedule/helpers/trainNameHelper';
1414
import { setFailure, setSuccess } from 'reducers/main';
1515
import { useAppDispatch } from 'store';
16-
import { formatToIsoDate, isoDateToMs, isoDateWithTimezoneToSec } from 'utils/date';
16+
import { isoDateToMs, isoDateWithTimezoneToSec } from 'utils/date';
1717
import { castErrorToFailure } from 'utils/error';
1818
import { sec2time } from 'utils/timeManipulation';
1919

@@ -58,16 +58,10 @@ const AddTrainScheduleButton = ({
5858
let actualTrainCount = 1;
5959

6060
for (let nb = 1; nb <= trainCount; nb += 1) {
61-
const newStartTimeString = formatToIsoDate(
62-
formattedStartTimeMs + 1000 * 60 * trainDelta * (nb - 1)
63-
);
61+
const newStartTime = new Date(formattedStartTimeMs + 1000 * 60 * trainDelta * (nb - 1));
6462
const trainName = trainNameWithNum(baseTrainName, actualTrainCount, trainCount);
6563

66-
const trainSchedule = formatTrainSchedulePayload(
67-
validTrainConfig,
68-
trainName,
69-
newStartTimeString
70-
);
64+
const trainSchedule = formatTrainSchedulePayload(validTrainConfig, trainName, newStartTime);
7165
trainScheduleParams.push({ ...trainSchedule });
7266
actualTrainCount += trainStep;
7367
}

front/src/modules/trainschedule/components/ManageTrainSchedule/TrainSettings.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ChipsSNCF from 'common/BootstrapSNCF/ChipsSNCF';
1111
import InputSNCF from 'common/BootstrapSNCF/InputSNCF';
1212
import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext';
1313
import { useAppDispatch } from 'store';
14-
import { dateTimeToIso } from 'utils/date';
14+
import { parseLocalDateTime, formatLocalDateTime } from 'utils/date';
1515
import { useDebounce } from 'utils/helpers';
1616
import { isInvalidFloatNumber } from 'utils/numbers';
1717

@@ -27,7 +27,7 @@ export default function TrainSettings() {
2727
const startTimeFromStore = useSelector(getStartTime);
2828

2929
const [name, setName] = useState<string>(nameFromStore);
30-
const [startTime, setStartTime] = useState(startTimeFromStore.substring(0, 19));
30+
const [startTime, setStartTime] = useState(formatLocalDateTime(startTimeFromStore));
3131
const [initialSpeed, setInitialSpeed] = useState<number | undefined>(initialSpeedFromStore);
3232
const dispatch = useAppDispatch();
3333

@@ -52,8 +52,8 @@ export default function TrainSettings() {
5252
}, [debouncedName]);
5353

5454
useEffect(() => {
55-
const formatedStartTime = dateTimeToIso(debouncedStartTime);
56-
if (formatedStartTime) dispatch(updateStartTime(formatedStartTime));
55+
const newStartTime = parseLocalDateTime(debouncedStartTime);
56+
if (newStartTime) dispatch(updateStartTime(newStartTime));
5757
}, [debouncedStartTime]);
5858

5959
useEffect(() => {
@@ -63,7 +63,7 @@ export default function TrainSettings() {
6363
useEffect(() => {
6464
setName(nameFromStore);
6565
setInitialSpeed(initialSpeedFromStore);
66-
setStartTime(startTimeFromStore.substring(0, 19));
66+
setStartTime(formatLocalDateTime(startTimeFromStore));
6767
}, [nameFromStore, initialSpeedFromStore, startTimeFromStore]);
6868

6969
const isInvalidTrainScheduleName = isInvalidName(name);

front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const checkCurrentConfig = (
149149
margins: formatMargin(compact(pathSteps)),
150150
schedule: formatSchedule(compact(pathSteps)),
151151
powerRestrictions: powerRestriction,
152-
firstStartTime: startTime,
152+
firstStartTime: startTime.toISOString(),
153153
speedLimitByTag,
154154
};
155155
};

front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { ValidConfig } from '../types';
66
export default function formatTrainSchedulePayload(
77
validConfig: ValidConfig,
88
trainName: string,
9-
startTime: string
9+
startTime: Date
1010
): TrainScheduleBase {
1111
const {
1212
constraintDistribution,
@@ -35,7 +35,7 @@ export default function formatTrainSchedulePayload(
3535
rolling_stock_name: rollingStockName,
3636
schedule: validConfig.schedule,
3737
speed_limit_tag: speedLimitByTag,
38-
start_time: startTime,
38+
start_time: startTime.toISOString(),
3939
train_name: trainName,
4040
};
4141
}

front/src/reducers/index.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const saveUserFilter = createFilter('user', userWhiteList);
5858
const saveMainFilter = createFilter('main', mainWhiteList);
5959

6060
// Deserialize date strings coming from local storage
61-
const dateTransform = createTransform(
61+
const stdcmPathStepsDateTransform = createTransform(
6262
null,
6363
(outboundState: { arrival?: string }[]) =>
6464
outboundState.map(({ arrival, ...step }) => {
@@ -69,14 +69,22 @@ const dateTransform = createTransform(
6969
}),
7070
{ whitelist: ['stdcmPathSteps'] }
7171
);
72+
const operationalStudiesDateTransform = createTransform(
73+
null,
74+
({ startTime, ...outboundState }: { startTime: string }) => ({
75+
...outboundState,
76+
startTime: new Date(startTime),
77+
}),
78+
{ whitelist: ['operationalStudiesConf'] }
79+
);
7280

7381
// Useful to only blacklist a sub-propertie of osrdconf
7482
const buildOsrdConfPersistConfig = <T extends OsrdConfState>(
7583
slice: ConfSlice
7684
): PersistConfig<T> => ({
7785
key: slice.name,
7886
storage,
79-
transforms: [dateTransform],
87+
transforms: [stdcmPathStepsDateTransform, operationalStudiesDateTransform],
8088
blacklist: ['featureInfoClick'],
8189
});
8290

front/src/reducers/osrdconf/operationalStudiesConf/index.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { TrainScheduleWithDetails } from 'modules/trainschedule/components/
55
import computeBasePathStep from 'modules/trainschedule/helpers/computeBasePathStep';
66
import { defaultCommonConf, buildCommonConfReducers } from 'reducers/osrdconf/osrdConfCommon';
77
import type { OsrdConfState } from 'reducers/osrdconf/types';
8-
import { convertIsoUtcToLocalTime } from 'utils/date';
98
import { msToKmh } from 'utils/physics';
109

1110
import { builPowerRestrictionReducer } from './powerRestrictionReducer';
@@ -27,7 +26,7 @@ export const operationalStudiesConfSlice = createSlice({
2726
rollingStock,
2827
trainName,
2928
initial_speed,
30-
start_time,
29+
startTime,
3130
options,
3231
speedLimitTag,
3332
labels,
@@ -38,7 +37,7 @@ export const operationalStudiesConfSlice = createSlice({
3837

3938
state.rollingStockID = rollingStock?.id;
4039
state.pathSteps = path.map((_, index) => computeBasePathStep(action.payload, index));
41-
state.startTime = convertIsoUtcToLocalTime(start_time);
40+
state.startTime = startTime;
4241

4342
state.name = trainName;
4443
state.initialSpeed = initial_speed ? Math.floor(msToKmh(initial_speed) * 10) / 10 : 0;

front/src/reducers/osrdconf/operationalStudiesConf/operationalStudiesConfReducer.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('simulationConfReducer', () => {
8888
receptionSignal: undefined,
8989
},
9090
],
91-
startTime: '2021-01-01T00:00:00+00:00',
91+
startTime: new Date('2021-01-01T00:00:00+00:00'),
9292
});
9393
});
9494

front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ const testCommonConfReducers = (slice: OperationalStudiesConfSlice | StdcmConfSl
255255
});
256256

257257
it('should handle updateStartTime', () => {
258-
const newStartTime = '2024-05-01T11:08:00.000+01:00';
258+
const newStartTime = new Date('2024-05-01T11:08:00.000+01:00');
259259
defaultStore.dispatch(slice.actions.updateStartTime(newStartTime));
260260
const state = defaultStore.getState()[slice.name];
261261
expect(state.startTime).toBe(newStartTime);

front/src/reducers/osrdconf/osrdConfCommon/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const defaultCommonConf: OsrdConfState = {
3737
// Corresponds to origin and destination not defined
3838
pathSteps: [null, null],
3939
rollingStockComfort: 'STANDARD' as const,
40-
startTime: new Date().toISOString(),
40+
startTime: new Date(),
4141
};
4242

4343
interface CommonConfReducers<S extends OsrdConfState> extends InfraStateReducers<S> {

front/src/reducers/osrdconf/types.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ export interface OsrdConfState extends InfraState {
4242
featureInfoClick: { displayPopup: boolean; feature?: Feature; coordinates?: number[] };
4343
pathSteps: (PathStep | null)[];
4444
rollingStockComfort: Comfort;
45-
// Format ISO 8601
46-
startTime: string;
45+
startTime: Date;
4746
}
4847

4948
export interface StandardAllowance {

front/src/utils/__tests__/date.spec.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
import { describe, it, expect } from 'vitest';
22

33
import {
4-
dateTimeToIso,
4+
parseLocalDateTime,
55
isoDateToMs,
66
formatToIsoDate,
77
serializeDateTimeWithoutYear,
88
extractDateAndTime,
99
isArrivalDateInSearchTimeWindow,
1010
} from 'utils/date';
1111

12-
describe('dateTimeToIso', () => {
12+
describe('parseLocalDateTime', () => {
1313
it('should return an iso date by passing a date without milliseconds', () => {
1414
const inputDate = '2024-04-25T08:20';
15-
const isoDate = dateTimeToIso(inputDate);
16-
expect(isoDate).toEqual('2024-04-25T08:20:00Z'); // Ends by Z because CI seems to be in UTC timezone
15+
const isoDate = parseLocalDateTime(inputDate);
16+
expect(isoDate?.toISOString()).toEqual('2024-04-25T08:20:00.000Z');
1717
});
1818

1919
it('should return an iso date by passing a date with milliseconds', () => {
2020
const inputDate = '2024-04-25T08:20:10';
21-
const isoDate = dateTimeToIso(inputDate);
22-
expect(isoDate).toEqual('2024-04-25T08:20:10Z'); // Ends by Z because CI seems to be in UTC timezone
21+
const isoDate = parseLocalDateTime(inputDate);
22+
expect(isoDate?.toISOString()).toEqual('2024-04-25T08:20:10.000Z');
2323
});
2424

2525
it('should return an iso date by passing a date with a space between date and time instead of a T', () => {
2626
const inputDate = '2024-04-25 08:20:10';
27-
const isoDate = dateTimeToIso(inputDate);
28-
expect(isoDate).toEqual('2024-04-25T08:20:10Z'); // Ends by Z because CI seems to be in UTC timezone
27+
const isoDate = parseLocalDateTime(inputDate);
28+
expect(isoDate?.toISOString()).toEqual('2024-04-25T08:20:10.000Z');
2929
});
3030

3131
it('should return null by passing a date with the wrong format', () => {
3232
const inputDate = '04-25 08:20:10';
33-
const isoDate = dateTimeToIso(inputDate);
33+
const isoDate = parseLocalDateTime(inputDate);
3434
expect(isoDate).toBeNull();
3535
});
3636
});

front/src/utils/date.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import utc from 'dayjs/plugin/utc';
66
import i18next from 'i18next';
77

88
import type { ScheduleConstraint } from 'applications/stdcm/types';
9-
import type { IsoDateTimeString } from 'common/types';
109
import i18n from 'i18n';
1110

1211
dayjs.extend(utc);
@@ -43,15 +42,18 @@ export function dateTimeFormatting(date: Date, withoutTime: boolean = false) {
4342
* @param inputDate e.g. 2024-04-25T08:30
4443
* @return an ISO 8601 date (e.g. 2024-04-25T08:30:00+02:00) or null
4544
*/
46-
export const dateTimeToIso = (inputDateTime: string) => {
45+
export const parseLocalDateTime = (inputDateTime: string) => {
4746
// Regex to check format 1234-56-78T12:00:00(:00)
4847
const inputDateTimeRegex = /^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}(?::\d{2}){0,1}$/;
4948
if (inputDateTimeRegex.test(inputDateTime)) {
50-
return dayjs.tz(inputDateTime, userTimeZone).format();
49+
return dayjs.tz(inputDateTime, userTimeZone).toDate();
5150
}
5251
return null;
5352
};
5453

54+
export const formatLocalDateTime = (date: Date) =>
55+
dayjs(date).local().format('YYYY-MM-DDTHH:mm:ss');
56+
5557
/**
5658
* Transform a milliseconds date to an ISO 8601 date with the user timezone
5759
* @param msDate milliseconds date (elapsed from January 1st 1970)
@@ -132,10 +134,6 @@ export function convertUTCDateToLocalDate(date: number) {
132134
return Math.abs(timeDifferenceMinutes) * 60 + date;
133135
}
134136

135-
export function convertIsoUtcToLocalTime(isoUtcString: IsoDateTimeString): string {
136-
return dayjs(isoUtcString).local().format();
137-
}
138-
139137
export function addDurationToDate(
140138
startTime: Date,
141139
duration: number,

0 commit comments

Comments
 (0)