Skip to content

Commit 64bcc82

Browse files
committed
front: refacto timetable filters
No change of filters behavior. - move all filter states in useFilterTrainSchedules - move useFilterTrainSchedules in Timetable component - remove useEffects in the custom hook in favor of useMemos - remove displayedTimetableItems state and reuse the filtered array from the custom hook - group the filters to a single object to facilitate the props drilling - rename some ref to train schedule in timetable item Signed-off-by: SharglutDev <p.filimon75@gmail.com>
1 parent 05dae8a commit 64bcc82

File tree

7 files changed

+178
-193
lines changed

7 files changed

+178
-193
lines changed

front/src/modules/trainschedule/components/Timetable/FilterPanel.tsx

+19-30
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,30 @@ import { X } from '@osrd-project/ui-icons';
33
import cx from 'classnames';
44
import { useTranslation } from 'react-i18next';
55

6-
import type { ValidityFilter, ScheduledPointsHonoredFilter } from './types';
6+
import type { ValidityFilter, ScheduledPointsHonoredFilter, TimetableFilters } from './types';
77

88
type FilterPanelProps = {
99
toggleFilterPanel: () => void;
10-
filter: string;
11-
setFilter: (filter: string) => void;
12-
rollingStockFilter: string;
13-
setRollingStockFilter: (rollingStockFilter: string) => void;
14-
validityFilter: ValidityFilter;
15-
setValidityFilter: (validityFilter: ValidityFilter) => void;
16-
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter;
17-
setScheduledPointsHonoredFilter: (
18-
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter
19-
) => void;
20-
uniqueTags: string[];
21-
selectedTags: Set<string | null>;
22-
setSelectedTags: React.Dispatch<React.SetStateAction<Set<string | null>>>;
10+
timetableFilters: TimetableFilters;
2311
};
2412

25-
const FilterPanel = ({
26-
toggleFilterPanel,
27-
filter,
28-
setFilter,
29-
rollingStockFilter,
30-
setRollingStockFilter,
31-
validityFilter,
32-
setValidityFilter,
33-
scheduledPointsHonoredFilter,
34-
setScheduledPointsHonoredFilter,
35-
uniqueTags,
36-
selectedTags,
37-
setSelectedTags,
38-
}: FilterPanelProps) => {
13+
const FilterPanel = ({ toggleFilterPanel, timetableFilters }: FilterPanelProps) => {
3914
const { t } = useTranslation('operationalStudies/scenario');
4015

16+
const {
17+
nameLabelFilter,
18+
setNameLabelFilter,
19+
rollingStockFilter,
20+
setRollingStockFilter,
21+
validityFilter,
22+
setValidityFilter,
23+
scheduledPointsHonoredFilter,
24+
setScheduledPointsHonoredFilter,
25+
uniqueTags,
26+
selectedTags,
27+
setSelectedTags,
28+
} = timetableFilters;
29+
4130
const validityOptions: { value: ValidityFilter; label: string }[] = [
4231
{ value: 'both', label: t('timetable.showAllTrains') },
4332
{ value: 'valid', label: t('timetable.showValidTrains') },
@@ -80,8 +69,8 @@ const FilterPanel = ({
8069
id="timetable-label-filter"
8170
name="timetable-label-filter"
8271
label={t('timetable.filterLabel')}
83-
value={filter}
84-
onChange={(e) => setFilter(e.target.value)}
72+
value={nameLabelFilter}
73+
onChange={(e) => setNameLabelFilter(e.target.value)}
8574
placeholder={t('filterPlaceholder')}
8675
data-testid="timetable-label-filter"
8776
title={t('filterPlaceholder')}

front/src/modules/trainschedule/components/Timetable/Timetable.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import PacedTrainItem from './PacedTrain/PacedTrainItem';
3030
import TimetableToolbar from './TimetableToolbar';
3131
import TrainScheduleItem from './TrainScheduleItem';
3232
import type { PacedTrainWithResult, TimetableItemResult, TrainScheduleWithDetails } from './types';
33+
import useFilterTimetableItems from './useFilterTimetableItems';
3334

3435
type TimetableProps = {
3536
setDisplayTrainScheduleManagement: (mode: string) => void;
@@ -61,7 +62,6 @@ const Timetable = ({
6162
const { t } = useTranslation(['operationalStudies/scenario', 'common/itemTypes']);
6263
const showPacedTrains = useSelector(getShowPacedTrains);
6364

64-
const [displayedTimetableItems, setDisplayedTimetableItems] = useState<TimetableItemResult[]>([]);
6565
const [conflictsListExpanded, setConflictsListExpanded] = useState(false);
6666
const [selectedTimetableItemIds, setSelectedTimetableItemIds] = useState<TimetableItemId[]>([]);
6767
const [showTrainDetails, setShowTrainDetails] = useState(false);
@@ -80,6 +80,8 @@ const Timetable = ({
8080
dtoImport();
8181
}, []);
8282

83+
const { filteredTimetableItems, ...timetableFilters } = useFilterTimetableItems(timetableItems);
84+
8385
const toggleConflictsListExpanded = () => {
8486
setConflictsListExpanded(!conflictsListExpanded);
8587
};
@@ -111,8 +113,8 @@ const Timetable = ({
111113
};
112114

113115
const currentDepartureDates = useMemo(
114-
() => displayedTimetableItems.map((train) => formatDepartureDate(train.startTime)),
115-
[displayedTimetableItems]
116+
() => filteredTimetableItems.map((train) => formatDepartureDate(train.startTime)),
117+
[filteredTimetableItems]
116118
);
117119

118120
const showDepartureDates = useMemo(() => {
@@ -180,16 +182,16 @@ const Timetable = ({
180182
showTrainDetails={showTrainDetails}
181183
toggleShowTrainDetails={toggleShowTrainDetails}
182184
timetableItems={timetableItems}
183-
displayedTimetableItems={displayedTimetableItems}
184-
setDisplayedTimetableItems={setDisplayedTimetableItems}
185+
filteredTimetableItems={filteredTimetableItems}
186+
timetableFilters={timetableFilters}
185187
selectedTimetableItemIds={selectedTimetableItemIds}
186188
setSelectedTimetableItemIds={setSelectedTimetableItemIds}
187189
removeTrains={removeAndUnselectTrains}
188190
trainSchedules={trainSchedules}
189191
isInSelection={selectedTimetableItemIds.length > 0}
190192
/>
191193
<Virtualizer overscan={15}>
192-
{displayedTimetableItems.map((timetableItem, index) => (
194+
{filteredTimetableItems.map((timetableItem, index) => (
193195
<div key={`timetable-train-card-${timetableItem.id}`}>
194196
{showDepartureDates[index] && (
195197
<div className="scenario-timetable-departure-date">
@@ -239,8 +241,8 @@ const Timetable = ({
239241
toggleConflictsList={toggleConflictsListExpanded}
240242
// TODO PACED TRAIN : Adapt this props to handle paced trains in issue
241243
trainSchedulesDetails={
242-
displayedTimetableItems.filter((train) =>
243-
isTrainSchedule(train.id)
244+
filteredTimetableItems.filter((timetableItem) =>
245+
isTrainSchedule(timetableItem.id)
244246
) as TrainScheduleWithDetails[]
245247
}
246248
onConflictClick={handleConflictClick}

front/src/modules/trainschedule/components/Timetable/TimetableToolbar.tsx

+10-44
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,18 @@ import { updateSelectedTrainId } from 'reducers/simulationResults';
2020
import { getSelectedTrainId } from 'reducers/simulationResults/selectors';
2121
import { useAppDispatch } from 'store';
2222
import { castErrorToFailure } from 'utils/error';
23-
import { useDebounce } from 'utils/helpers';
2423
import { formatTrainScheduleIdToEditoastTrainId, isTrainSchedule } from 'utils/trainId';
2524

2625
import FilterPanel from './FilterPanel';
27-
import type { ScheduledPointsHonoredFilter, TimetableItemResult, ValidityFilter } from './types';
28-
import useFilterTrainSchedules from './useFilterTrainSchedules';
29-
import { timetableHasInvalidTrain } from './utils';
26+
import type { TimetableFilters, TimetableItemResult } from './types';
27+
import { timetableHasInvalidItem } from './utils';
3028

3129
type TimetableToolbarProps = {
3230
showTrainDetails: boolean;
3331
toggleShowTrainDetails: () => void;
3432
timetableItems: TimetableItemResult[];
35-
displayedTimetableItems: TimetableItemResult[];
36-
setDisplayedTimetableItems: (trainSchedulesDetails: TimetableItemResult[]) => void;
33+
filteredTimetableItems: TimetableItemResult[];
34+
timetableFilters: TimetableFilters;
3735
selectedTimetableItemIds: TimetableItemId[];
3836
setSelectedTimetableItemIds: (selectedTimetableIds: TimetableItemId[]) => void;
3937
removeTrains: (trainIds: TimetableItemId[]) => void;
@@ -45,8 +43,8 @@ const TimetableToolbar = ({
4543
showTrainDetails,
4644
toggleShowTrainDetails,
4745
timetableItems,
48-
displayedTimetableItems,
49-
setDisplayedTimetableItems,
46+
filteredTimetableItems,
47+
timetableFilters,
5048
selectedTimetableItemIds,
5149
setSelectedTimetableItemIds,
5250
removeTrains,
@@ -61,13 +59,6 @@ const TimetableToolbar = ({
6159

6260
const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false);
6361

64-
const [filter, setFilter] = useState('');
65-
const [rollingStockFilter, setRollingStockFilter] = useState('');
66-
const [validityFilter, setValidityFilter] = useState<ValidityFilter>('both');
67-
const [scheduledPointsHonoredFilter, setScheduledPointsHonoredFilter] =
68-
useState<ScheduledPointsHonoredFilter>('both');
69-
const [selectedTags, setSelectedTags] = useState<Set<string | null>>(new Set());
70-
7162
const { selectedTrainScheduleIds, selectedPacedTrainIds } = useMemo(
7263
() =>
7364
selectedTimetableItemIds.reduce(
@@ -103,32 +94,17 @@ const TimetableToolbar = ({
10394
[timetableItems]
10495
);
10596

106-
const debouncedFilter = useDebounce(filter, 500);
107-
108-
const debouncedRollingstockFilter = useDebounce(rollingStockFilter, 500);
109-
11097
const [deleteTrainSchedules] = osrdEditoastApi.endpoints.deleteTrainSchedule.useMutation();
11198

112-
// TODO: move this hook in Timetable
113-
const { uniqueTags } = useFilterTrainSchedules(
114-
timetableItems,
115-
debouncedFilter,
116-
debouncedRollingstockFilter,
117-
validityFilter,
118-
scheduledPointsHonoredFilter,
119-
selectedTags,
120-
setDisplayedTimetableItems
121-
);
122-
12399
const toggleFilterPanel = () => {
124100
setIsFilterPanelOpen(!isFilterPanelOpen);
125101
};
126102

127103
const toggleAllTrainsSelecton = () => {
128-
if (displayedTimetableItems.length === selectedTimetableItemIds.length) {
104+
if (filteredTimetableItems.length === selectedTimetableItemIds.length) {
129105
setSelectedTimetableItemIds([]);
130106
} else {
131-
const timetableItemsDisplayed = displayedTimetableItems.map(({ id }) => id);
107+
const timetableItemsDisplayed = filteredTimetableItems.map(({ id }) => id);
132108
setSelectedTimetableItemIds(timetableItemsDisplayed);
133109
}
134110
};
@@ -303,7 +279,7 @@ const TimetableToolbar = ({
303279
</div>
304280
)}
305281
</div>
306-
{timetableHasInvalidTrain(displayedTimetableItems) && (
282+
{timetableHasInvalidItem(filteredTimetableItems) && (
307283
<div className="invalid-trains">
308284
<Alert size="sm" variant="fill" />
309285
<span data-testid="invalid-trains-message" className="invalid-trains-message">
@@ -332,17 +308,7 @@ const TimetableToolbar = ({
332308
) : (
333309
<FilterPanel
334310
toggleFilterPanel={toggleFilterPanel}
335-
filter={filter}
336-
setFilter={setFilter}
337-
rollingStockFilter={rollingStockFilter}
338-
setRollingStockFilter={setRollingStockFilter}
339-
validityFilter={validityFilter}
340-
setValidityFilter={setValidityFilter}
341-
scheduledPointsHonoredFilter={scheduledPointsHonoredFilter}
342-
setScheduledPointsHonoredFilter={setScheduledPointsHonoredFilter}
343-
uniqueTags={uniqueTags}
344-
selectedTags={selectedTags}
345-
setSelectedTags={setSelectedTags}
311+
timetableFilters={timetableFilters}
346312
/>
347313
)}
348314
</div>

front/src/modules/trainschedule/components/Timetable/types.ts

+16
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,19 @@ export type PacedTrainWithResult = TimetableItemWithDetails & {
5757
};
5858

5959
export type TimetableItemResult = TrainScheduleWithDetails | PacedTrainWithResult;
60+
61+
export type TimetableFilters = {
62+
uniqueTags: string[];
63+
nameLabelFilter: string;
64+
setNameLabelFilter: (nameLabelFilter: string) => void;
65+
rollingStockFilter: string;
66+
setRollingStockFilter: (rollingStockFilter: string) => void;
67+
validityFilter: ValidityFilter;
68+
setValidityFilter: (validityFilter: ValidityFilter) => void;
69+
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter;
70+
setScheduledPointsHonoredFilter: (
71+
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter
72+
) => void;
73+
selectedTags: Set<string | null>;
74+
setSelectedTags: React.Dispatch<React.SetStateAction<Set<string | null>>>;
75+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { useMemo, useState } from 'react';
2+
3+
import { uniq } from 'lodash';
4+
5+
import { useDebounce } from 'utils/helpers';
6+
7+
import type {
8+
ScheduledPointsHonoredFilter,
9+
TimetableFilters,
10+
TimetableItemResult,
11+
ValidityFilter,
12+
} from './types';
13+
import { extractTagCode, keepItem } from './utils';
14+
15+
/**
16+
* Hook filtering a timetable items array depending on some filters
17+
* @param timetableItems the timetable's items
18+
* @returns all filters, their setters, the unique speed limit tags among all items and the filtered timetable items
19+
*/
20+
const useFilterTimetableItems = (
21+
timetableItems: TimetableItemResult[]
22+
): TimetableFilters & { filteredTimetableItems: TimetableItemResult[] } => {
23+
const [nameLabelFilter, setNameLabelFilter] = useState('');
24+
const [rollingStockFilter, setRollingStockFilter] = useState('');
25+
const [validityFilter, setValidityFilter] = useState<ValidityFilter>('both');
26+
const [scheduledPointsHonoredFilter, setScheduledPointsHonoredFilter] =
27+
useState<ScheduledPointsHonoredFilter>('both');
28+
const [selectedTags, setSelectedTags] = useState<Set<string | null>>(new Set());
29+
30+
const debouncedNameLabelFilter = useDebounce(nameLabelFilter, 500);
31+
const debouncedRollingstockFilter = useDebounce(rollingStockFilter, 500);
32+
33+
const uniqueTags = useMemo(
34+
() => uniq(timetableItems.map((timetableItem) => extractTagCode(timetableItem.speedLimitTag))),
35+
[timetableItems]
36+
);
37+
38+
const filteredTimetableItems: TimetableItemResult[] = useMemo(
39+
() =>
40+
timetableItems.filter((timetableItem) => {
41+
if (!keepItem(timetableItem, debouncedNameLabelFilter)) return false;
42+
43+
// Apply validity filter
44+
if (validityFilter !== 'both') {
45+
if (validityFilter === 'valid' && !timetableItem.isValid) return false;
46+
if (validityFilter === 'invalid' && timetableItem.isValid) return false;
47+
}
48+
49+
// Apply scheduled points honored filter
50+
if (scheduledPointsHonoredFilter !== 'both') {
51+
if (!timetableItem.isValid) {
52+
return false;
53+
}
54+
const { scheduledPointsNotHonored } = timetableItem;
55+
if (
56+
(scheduledPointsHonoredFilter === 'honored' && scheduledPointsNotHonored) ||
57+
(scheduledPointsHonoredFilter === 'notHonored' && !scheduledPointsNotHonored)
58+
) {
59+
return false;
60+
}
61+
}
62+
63+
// Apply tag filter
64+
if (
65+
selectedTags.size > 0 &&
66+
!selectedTags.has(extractTagCode(timetableItem.speedLimitTag))
67+
) {
68+
return false;
69+
}
70+
71+
// Apply rolling stock filter
72+
if (debouncedRollingstockFilter) {
73+
const {
74+
detail = '',
75+
family = '',
76+
reference = '',
77+
series = '',
78+
subseries = '',
79+
} = timetableItem.rollingStock?.metadata || {};
80+
if (
81+
![detail, family, reference, series, subseries].some((v) =>
82+
v.toLowerCase().includes(debouncedRollingstockFilter.toLowerCase())
83+
)
84+
)
85+
return false;
86+
}
87+
88+
return true;
89+
}),
90+
[
91+
timetableItems,
92+
debouncedNameLabelFilter,
93+
debouncedRollingstockFilter,
94+
validityFilter,
95+
scheduledPointsHonoredFilter,
96+
selectedTags,
97+
]
98+
);
99+
100+
return {
101+
filteredTimetableItems,
102+
uniqueTags,
103+
nameLabelFilter,
104+
setNameLabelFilter,
105+
rollingStockFilter,
106+
setRollingStockFilter,
107+
validityFilter,
108+
setValidityFilter,
109+
scheduledPointsHonoredFilter,
110+
setScheduledPointsHonoredFilter,
111+
selectedTags,
112+
setSelectedTags,
113+
};
114+
};
115+
116+
export default useFilterTimetableItems;

0 commit comments

Comments
 (0)