Skip to content

Commit fc0cd2e

Browse files
committed
front: use new spreadsheet component
1 parent 43ed182 commit fc0cd2e

File tree

11 files changed

+150
-169
lines changed

11 files changed

+150
-169
lines changed

front/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"react": "^18.2.0",
8686
"react-beautiful-dnd": "^13.1.1",
8787
"react-countdown": "^2.3.5",
88+
"react-datasheet-grid": "^4.11.4",
8889
"react-dom": "^18.2.0",
8990
"react-flatpickr": "^3.10.13",
9091
"react-hook-form": "^7.50.0",
@@ -100,7 +101,6 @@
100101
"react-rnd": "^10.4.1",
101102
"react-router-dom": "^6.22.0",
102103
"react-select": "^5.8.0",
103-
"react-spreadsheet": "^0.9.4",
104104
"react-tether": "^3.0.3",
105105
"react-transition-group": "^4.4.5",
106106
"redux": "^5.0.1",

front/src/modules/rollingStock/components/RollingStockEditor/CurveSpreadsheet.tsx

+73-49
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import React, { type Dispatch, type SetStateAction, useMemo } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+
import type { Dispatch, SetStateAction } from 'react';
23

4+
import { DataSheetGrid, keyColumn, intColumn, floatColumn } from 'react-datasheet-grid';
5+
import 'react-datasheet-grid/dist/style.css';
36
import { useTranslation } from 'react-i18next';
4-
import Spreadsheet, { createEmptyMatrix } from 'react-spreadsheet';
5-
import type { CellBase, Matrix } from 'react-spreadsheet';
67

7-
import type { ConditionalEffortCurveForm, EffortCurveForms } from 'modules/rollingStock/types';
8+
import type {
9+
ConditionalEffortCurveForm,
10+
DataSheetCurve,
11+
EffortCurveForms,
12+
} from 'modules/rollingStock/types';
813
import { replaceElementAtIndex } from 'utils/array';
914
import { msToKmh } from 'utils/physics';
1015

@@ -14,9 +19,9 @@ type CurveSpreadsheetProps = {
1419
selectedCurve: ConditionalEffortCurveForm;
1520
selectedCurveIndex: number;
1621
selectedTractionModeCurves: ConditionalEffortCurveForm[];
17-
effortCurves: EffortCurveForms | null;
22+
effortCurves: EffortCurveForms;
1823
setEffortCurves: Dispatch<SetStateAction<EffortCurveForms | null>>;
19-
selectedTractionMode: string | null;
24+
selectedTractionMode: string;
2025
isDefaultCurve: boolean;
2126
};
2227

@@ -30,45 +35,38 @@ const CurveSpreadsheet = ({
3035
isDefaultCurve,
3136
}: CurveSpreadsheetProps) => {
3237
const { t } = useTranslation('rollingstock');
38+
const columns = useMemo(
39+
() => [
40+
{ ...keyColumn('speed', intColumn), title: t('speed') },
41+
{ ...keyColumn('effort', floatColumn), title: t('effort') },
42+
],
43+
[t]
44+
);
3345

34-
const spreadsheetCurve = useMemo(() => {
35-
const { speeds, max_efforts } = selectedCurve.curve;
36-
const filledMatrix: (
37-
| {
38-
value: string;
39-
}
40-
| undefined
41-
)[][] =
42-
speeds && max_efforts
43-
? max_efforts.map((effort, index) => [
44-
{
45-
value:
46-
speeds[index] !== undefined ? Math.round(msToKmh(speeds[index]!)).toString() : '',
47-
},
48-
// Effort needs to be displayed in kN
49-
{ value: effort !== undefined ? Math.round(effort / 1000).toString() : '' },
50-
])
51-
: [];
52-
const numberOfRows = filledMatrix.length < 8 ? 8 - filledMatrix.length : 1;
53-
return filledMatrix.concat(createEmptyMatrix<CellBase<string>>(numberOfRows, 2));
54-
}, [selectedCurve]);
46+
const [needsSort, setNeedsSort] = useState<boolean>(false);
5547

56-
const updateRollingStockCurve = (e: Matrix<{ value: string }>) => {
57-
if (!selectedTractionMode || !effortCurves) return;
58-
const formattedCurve = formatCurve(e);
48+
const handleBlur = useCallback(() => {
49+
setNeedsSort(true);
50+
}, []);
5951

52+
const updateRollingStockCurve = (newCurve: DataSheetCurve[]) => {
53+
// Format the new curve
54+
const formattedCurve = formatCurve(newCurve);
55+
56+
// Create the updated selected curve
6057
const updatedSelectedCurve = {
6158
...selectedCurve,
6259
curve: formattedCurve,
6360
};
6461

65-
// replace the updated curve
62+
// Replace the updated curve in the selected traction mode curves
6663
const updatedCurves = replaceElementAtIndex(
6764
selectedTractionModeCurves,
6865
selectedCurveIndex,
6966
updatedSelectedCurve
7067
);
7168

69+
// Update the effort curves
7270
const updatedEffortCurve = {
7371
...effortCurves,
7472
[selectedTractionMode]: {
@@ -77,30 +75,56 @@ const CurveSpreadsheet = ({
7775
...(isDefaultCurve ? { default_curve: formattedCurve } : {}),
7876
},
7977
};
78+
8079
setEffortCurves(updatedEffortCurve);
8180
};
8281

83-
const orderSpreadsheetValues = () => {
84-
const orderedValuesByVelocity = spreadsheetCurve.sort((a, b) => {
85-
// if a row has a max_effort, but no speed, it should appear at the top of the table
86-
if (b[0] && b[0].value === '') return 1;
87-
return Number(a[0]?.value) - Number(b[0]?.value);
88-
});
89-
updateRollingStockCurve(orderedValuesByVelocity);
90-
};
82+
const spreadsheetCurve = useMemo(() => {
83+
const { speeds, max_efforts } = selectedCurve.curve;
84+
85+
const filledDataSheet: DataSheetCurve[] = max_efforts.map((effort, index) => ({
86+
speed: speeds[index] !== null ? Math.round(msToKmh(speeds[index]!)) : null,
87+
// Effort needs to be displayed in kN
88+
effort: effort && effort !== null ? effort / 1000 : null,
89+
}));
90+
91+
// Add an empty line for input only if last line is not already empty
92+
if (
93+
filledDataSheet.length === 0 ||
94+
filledDataSheet[filledDataSheet.length - 1].speed !== null ||
95+
filledDataSheet[filledDataSheet.length - 1].effort !== null
96+
) {
97+
filledDataSheet.push({ speed: null, effort: null });
98+
}
99+
100+
return filledDataSheet;
101+
}, [selectedCurve]);
102+
103+
useEffect(() => {
104+
if (needsSort) {
105+
const sortedSpreadsheetValues = spreadsheetCurve
106+
.filter((item) => item.speed !== null || item.effort !== null)
107+
.sort((a, b) => {
108+
if (a.speed === null) return -1;
109+
if (b.speed === null) return 1;
110+
return Number(a.speed) - Number(b.speed);
111+
});
112+
113+
updateRollingStockCurve(sortedSpreadsheetValues);
114+
setNeedsSort(false);
115+
}
116+
}, [needsSort]);
91117

92118
return (
93119
<div className="rollingstock-editor-spreadsheet">
94-
<Spreadsheet
95-
data={spreadsheetCurve}
96-
onChange={(e) => {
97-
updateRollingStockCurve(e);
98-
}}
99-
onBlur={orderSpreadsheetValues}
100-
onKeyDown={(e) => {
101-
if (e.key === 'Enter') orderSpreadsheetValues();
102-
}}
103-
columnLabels={[t('speed'), t('effort')]}
120+
<DataSheetGrid
121+
value={spreadsheetCurve}
122+
columns={columns}
123+
onChange={(e) => updateRollingStockCurve(e as DataSheetCurve[])}
124+
rowHeight={30}
125+
addRowsComponent={false}
126+
onBlur={handleBlur}
127+
onSelectionChange={handleBlur}
104128
/>
105129
</div>
106130
);

front/src/modules/rollingStock/components/RollingStockEditor/RollingStockEditorCurves.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import CurveSpreadsheet from 'modules/rollingStock/components/RollingStockEditor
1515
import { STANDARD_COMFORT_LEVEL, THERMAL_TRACTION_IDENTIFIER } from 'modules/rollingStock/consts';
1616
import { getElectricalProfilesAndPowerRestrictions } from 'modules/rollingStock/helpers/rollingStockEditor';
1717
import {
18-
filterUndefinedValueInCurve,
18+
filterNullValueInCurve,
1919
orderElectricalProfils,
2020
orderSelectorList,
2121
} from 'modules/rollingStock/helpers/utils';
@@ -176,7 +176,7 @@ const RollingStockEditorCurves = ({
176176
...selectedModeCurves,
177177
curves: matchingCurves.map((condCurve) => ({
178178
...condCurve,
179-
curve: filterUndefinedValueInCurve(condCurve.curve),
179+
curve: filterNullValueInCurve(condCurve.curve),
180180
})),
181181
},
182182
} as EffortCurves['modes'];
@@ -211,7 +211,8 @@ const RollingStockEditorCurves = ({
211211
{selectedTractionMode &&
212212
selectedCurve &&
213213
selectedCurveIndex !== null &&
214-
selectedTractionModeCurves && (
214+
selectedTractionModeCurves &&
215+
effortCurves && (
215216
<div className="rollingstock-editor-curves d-flex p-3">
216217
<CurveSpreadsheet
217218
selectedCurve={selectedCurve}
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,16 @@
1-
import type { Matrix } from 'react-spreadsheet';
2-
3-
import type { EffortCurveForm } from 'modules/rollingStock/types';
1+
import type { DataSheetCurve, EffortCurveForm } from 'modules/rollingStock/types';
42
import { kmhToMs } from 'utils/physics';
5-
import { emptyStringRegex, onlyDigit } from 'utils/strings';
6-
7-
/** Remove rows which have at least 1 empty cell */
8-
const filterUnvalidRows = (sheetValues: Matrix<{ value: string }>) =>
9-
sheetValues.filter(
10-
([a, b]) =>
11-
(a?.value && !emptyStringRegex.test(a.value)) || (b?.value && !emptyStringRegex.test(b.value))
12-
);
133

14-
/** For each cell, filter non digit characters */
15-
const removeNonDigitCharacters = (rows: Matrix<{ value: string }>) =>
16-
rows.map((row) => row.map((cell) => onlyDigit((cell?.value || '').replaceAll(',', '.'))));
17-
18-
const formatToEffortCurve = (rows: Matrix<string>) =>
4+
const formatCurve = (rows: DataSheetCurve[]) =>
195
rows.reduce<EffortCurveForm>(
206
(result, row) => {
21-
result.speeds.push(row[0] !== '' ? kmhToMs(Number(row[0])) : undefined);
7+
result.speeds.push(row.speed !== null ? kmhToMs(Number(row.speed)) : null);
228
// Back-end needs effort in newton
23-
result.max_efforts.push(row[1] !== '' ? Number(row[1]) * 1000 : undefined);
9+
result.max_efforts.push(row.effort !== null ? Number(row.effort) * 1000 : null);
10+
2411
return result;
2512
},
2613
{ max_efforts: [], speeds: [] }
2714
);
2815

29-
/**
30-
* Given a spreadsheet, return an EffortCurve
31-
* - remove rows which have at least 1 empty cell
32-
* - remove non digit characters in each cell
33-
* - convert rows data to EffortCurve
34-
*/
35-
export default function formatCurve(sheetValues: Matrix<{ value: string }>) {
36-
const validRows = filterUnvalidRows(sheetValues);
37-
const numericRows = removeNonDigitCharacters(validRows);
38-
return formatToEffortCurve(numericRows);
39-
}
16+
export default formatCurve;

front/src/modules/rollingStock/helpers/utils.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ import { kmhToMs, msToKmh } from 'utils/physics';
3232
import { getTranslationKey } from 'utils/strings';
3333
import type { ValueOf } from 'utils/types';
3434

35-
export const filterUndefinedValueInCurve = (curve: EffortCurveForm) =>
35+
export const filterNullValueInCurve = (curve: EffortCurveForm) =>
3636
curve.speeds.reduce<EffortCurve>(
3737
(result, speed, index) => {
3838
const maxEffort = curve.max_efforts[index];
39-
if (speed !== undefined && maxEffort !== undefined) {
39+
if (speed !== null && maxEffort !== null) {
4040
result.speeds.push(speed);
4141
result.max_efforts.push(maxEffort);
4242
}
@@ -132,11 +132,11 @@ export const rollingStockEditorQueryArg = (
132132
...acc,
133133
[mode]: {
134134
...currentRsEffortCurve[mode],
135-
default_curve: filterUndefinedValueInCurve(currentRsEffortCurve[mode].default_curve),
135+
default_curve: filterNullValueInCurve(currentRsEffortCurve[mode].default_curve),
136136
curves: [
137137
...currentRsEffortCurve[mode].curves.map((curve) => ({
138138
...curve,
139-
curve: filterUndefinedValueInCurve(curve.curve),
139+
curve: filterNullValueInCurve(curve.curve),
140140
})),
141141
],
142142
},
@@ -259,7 +259,7 @@ export const checkRollingStockFormValidity = (
259259
Object.entries(effortCurves || {}).forEach(([mode, { curves }]) => {
260260
curves.forEach(
261261
({ curve, cond: { comfort, electrical_profile_level, power_restriction_code } }) => {
262-
const filteredCurve = filterUndefinedValueInCurve(curve);
262+
const filteredCurve = filterNullValueInCurve(curve);
263263

264264
if (isInvalidCurve(filteredCurve)) {
265265
const formattedComfort = formatCurveCondition(comfort, t, 'comfortTypes');

front/src/modules/rollingStock/types.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,15 @@ export type ElectricalProfileByMode = {
122122
thermal: null[];
123123
};
124124

125+
export type DataSheetCurve = {
126+
speed: number | null;
127+
effort: number | null;
128+
};
129+
125130
// Effort curve with values number or undefined
126131
export type EffortCurveForm = {
127-
max_efforts: Array<number | undefined>;
128-
speeds: Array<number | undefined>;
132+
max_efforts: Array<number | null>;
133+
speeds: Array<number | null>;
129134
};
130135

131136
export type ConditionalEffortCurveForm = {

front/src/styles/scss/applications/rollingStockEditor/_rollingStockForm.scss

+4-11
Original file line numberDiff line numberDiff line change
@@ -223,19 +223,12 @@
223223

224224
.rollingstock-editor-spreadsheet {
225225
width: 40%;
226-
height: 20.75rem;
227-
overflow: auto;
228-
tr:not(:first-child) .Spreadsheet__header,
229-
tr:first-child .Spreadsheet__header:first-child {
230-
width: 2rem;
231-
border: solid 1px var(--coolgray3);
232-
}
233-
tr:not(:first-child) .Spreadsheet__header {
234-
background-color: var(--white);
235-
}
236-
tr:first-child .Spreadsheet__header {
226+
height: auto;
227+
border-radius: 0.3rem;
228+
.dsg-cell-header {
237229
color: var(--blue);
238230
white-space: normal !important;
231+
background-color: var(--coolgray1);
239232
}
240233
}
241234

front/src/utils/strings.ts

-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ export function snakeToCamel(str: string) {
3333
return str.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr: string) => chr.toUpperCase());
3434
}
3535

36-
export const emptyStringRegex = /^\s+$/;
37-
3836
export function getTranslationKey(translationList: string | undefined, item: string): string {
3937
return `${translationList ? `${translationList}.` : ''}${item}`;
4038
}

front/tests/009-rollingstock-editor.spec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,16 @@ test.describe('Rollingstock editor page', () => {
8484

8585
// Complete the speed effort curves Not specified
8686

87-
const velocityCellRow0 = playwrightRollingstockEditorPage.getVelocityCellByRow('0');
87+
const velocityCellRow0 = playwrightRollingstockEditorPage.getVelocityCellByRow(1);
8888
await playwrightRollingstockEditorPage.setSpreedsheetCell('0', velocityCellRow0);
8989

90-
const effortCellRow0 = playwrightRollingstockEditorPage.getEffortCellByRow('0');
90+
const effortCellRow0 = playwrightRollingstockEditorPage.getEffortCellByRow(1);
9191
await playwrightRollingstockEditorPage.setSpreedsheetCell('900', effortCellRow0);
9292

93-
const velocityCellRow1 = playwrightRollingstockEditorPage.getVelocityCellByRow('1');
93+
const velocityCellRow1 = playwrightRollingstockEditorPage.getVelocityCellByRow(2);
9494
await playwrightRollingstockEditorPage.setSpreedsheetCell('5', velocityCellRow1);
9595

96-
const effortCellRow1 = playwrightRollingstockEditorPage.getEffortCellByRow('1');
96+
const effortCellRow1 = playwrightRollingstockEditorPage.getEffortCellByRow(2);
9797
await playwrightRollingstockEditorPage.setSpreedsheetCell('800', effortCellRow1);
9898

9999
// Select and complete the speed effort curves C0
@@ -113,16 +113,16 @@ test.describe('Rollingstock editor page', () => {
113113

114114
await playwrightRollingstockEditorPage.setSpreedsheetCell('800', effortCellRow1);
115115

116-
const velocityCellRow2 = playwrightRollingstockEditorPage.getVelocityCellByRow('2');
116+
const velocityCellRow2 = playwrightRollingstockEditorPage.getVelocityCellByRow(3);
117117
await playwrightRollingstockEditorPage.setSpreedsheetCell('10', velocityCellRow2);
118118

119-
const effortCellRow2 = playwrightRollingstockEditorPage.getEffortCellByRow('2');
119+
const effortCellRow2 = playwrightRollingstockEditorPage.getEffortCellByRow(3);
120120
await playwrightRollingstockEditorPage.setSpreedsheetCell('900', effortCellRow2);
121121

122-
const velocityCellRow3 = playwrightRollingstockEditorPage.getVelocityCellByRow('3');
122+
const velocityCellRow3 = playwrightRollingstockEditorPage.getVelocityCellByRow(4);
123123
await playwrightRollingstockEditorPage.setSpreedsheetCell('20', velocityCellRow3);
124124

125-
const effortCellRow3 = playwrightRollingstockEditorPage.getEffortCellByRow('3');
125+
const effortCellRow3 = playwrightRollingstockEditorPage.getEffortCellByRow(4);
126126
await playwrightRollingstockEditorPage.setSpreedsheetCell('800', effortCellRow3);
127127

128128
await playwrightRollingstockEditorPage.clickOnRollingstockDetailsButton();

0 commit comments

Comments
 (0)