Skip to content

Commit

Permalink
front: refacto timetable filters
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
SharglutDev committed Feb 27, 2025
1 parent 4a0d36b commit 3176332
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,30 @@ import { X } from '@osrd-project/ui-icons';
import cx from 'classnames';
import { useTranslation } from 'react-i18next';

import type { ValidityFilter, ScheduledPointsHonoredFilter } from './types';
import type { ValidityFilter, ScheduledPointsHonoredFilter, TimetableFilters } from './types';

type FilterPanelProps = {
toggleFilterPanel: () => void;
filter: string;
setFilter: (filter: string) => void;
rollingStockFilter: string;
setRollingStockFilter: (rollingStockFilter: string) => void;
validityFilter: ValidityFilter;
setValidityFilter: (validityFilter: ValidityFilter) => void;
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter;
setScheduledPointsHonoredFilter: (
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter
) => void;
uniqueTags: string[];
selectedTags: Set<string | null>;
setSelectedTags: React.Dispatch<React.SetStateAction<Set<string | null>>>;
timetableFilters: TimetableFilters;
};

const FilterPanel = ({
toggleFilterPanel,
filter,
setFilter,
rollingStockFilter,
setRollingStockFilter,
validityFilter,
setValidityFilter,
scheduledPointsHonoredFilter,
setScheduledPointsHonoredFilter,
uniqueTags,
selectedTags,
setSelectedTags,
}: FilterPanelProps) => {
const FilterPanel = ({ toggleFilterPanel, timetableFilters }: FilterPanelProps) => {
const { t } = useTranslation('operationalStudies/scenario');

const {
nameLabelFilter,
setNameLabelFilter,
rollingStockFilter,
setRollingStockFilter,
validityFilter,
setValidityFilter,
scheduledPointsHonoredFilter,
setScheduledPointsHonoredFilter,
uniqueTags,
selectedTags,
setSelectedTags,
} = timetableFilters;

const validityOptions: { value: ValidityFilter; label: string }[] = [
{ value: 'both', label: t('timetable.showAllTrains') },
{ value: 'valid', label: t('timetable.showValidTrains') },
Expand Down Expand Up @@ -80,8 +69,8 @@ const FilterPanel = ({
id="timetable-label-filter"
name="timetable-label-filter"
label={t('timetable.filterLabel')}
value={filter}
onChange={(e) => setFilter(e.target.value)}
value={nameLabelFilter}
onChange={(e) => setNameLabelFilter(e.target.value)}
placeholder={t('filterPlaceholder')}
data-testid="timetable-label-filter"
title={t('filterPlaceholder')}
Expand Down
18 changes: 10 additions & 8 deletions front/src/modules/trainschedule/components/Timetable/Timetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import PacedTrainItem from './PacedTrain/PacedTrainItem';
import TimetableToolbar from './TimetableToolbar';
import TrainScheduleItem from './TrainScheduleItem';
import type { PacedTrainWithResult, TimetableItemResult, TrainScheduleWithDetails } from './types';
import useFilterTimetableItems from './useFilterTimetableItems';

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

const [displayedTimetableItems, setDisplayedTimetableItems] = useState<TimetableItemResult[]>([]);
const [conflictsListExpanded, setConflictsListExpanded] = useState(false);
const [selectedTimetableItemIds, setSelectedTimetableItemIds] = useState<TimetableItemId[]>([]);
const [showTrainDetails, setShowTrainDetails] = useState(false);
Expand All @@ -80,6 +80,8 @@ const Timetable = ({
dtoImport();
}, []);

const { filteredTimetableItems, ...timetableFilters } = useFilterTimetableItems(timetableItems);

const toggleConflictsListExpanded = () => {
setConflictsListExpanded(!conflictsListExpanded);
};
Expand Down Expand Up @@ -111,8 +113,8 @@ const Timetable = ({
};

const currentDepartureDates = useMemo(
() => displayedTimetableItems.map((train) => formatDepartureDate(train.startTime)),
[displayedTimetableItems]
() => filteredTimetableItems.map((train) => formatDepartureDate(train.startTime)),
[filteredTimetableItems]
);

const showDepartureDates = useMemo(() => {
Expand Down Expand Up @@ -180,16 +182,16 @@ const Timetable = ({
showTrainDetails={showTrainDetails}
toggleShowTrainDetails={toggleShowTrainDetails}
timetableItems={timetableItems}
displayedTimetableItems={displayedTimetableItems}
setDisplayedTimetableItems={setDisplayedTimetableItems}
filteredTimetableItems={filteredTimetableItems}
timetableFilters={timetableFilters}
selectedTimetableItemIds={selectedTimetableItemIds}
setSelectedTimetableItemIds={setSelectedTimetableItemIds}
removeTrains={removeAndUnselectTrains}
trainSchedules={trainSchedules}
isInSelection={selectedTimetableItemIds.length > 0}
/>
<Virtualizer overscan={15}>
{displayedTimetableItems.map((timetableItem, index) => (
{filteredTimetableItems.map((timetableItem, index) => (
<div key={`timetable-train-card-${timetableItem.id}`}>
{showDepartureDates[index] && (
<div className="scenario-timetable-departure-date">
Expand Down Expand Up @@ -239,8 +241,8 @@ const Timetable = ({
toggleConflictsList={toggleConflictsListExpanded}
// TODO PACED TRAIN : Adapt this props to handle paced trains in issue
trainSchedulesDetails={
displayedTimetableItems.filter((train) =>
isTrainSchedule(train.id)
filteredTimetableItems.filter((timetableItem) =>
isTrainSchedule(timetableItem.id)
) as TrainScheduleWithDetails[]
}
onConflictClick={handleConflictClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,18 @@ import { updateSelectedTrainId } from 'reducers/simulationResults';
import { getSelectedTrainId } from 'reducers/simulationResults/selectors';
import { useAppDispatch } from 'store';
import { castErrorToFailure } from 'utils/error';
import { useDebounce } from 'utils/helpers';
import { formatTrainScheduleIdToEditoastTrainId, isTrainSchedule } from 'utils/trainId';

import FilterPanel from './FilterPanel';
import type { ScheduledPointsHonoredFilter, TimetableItemResult, ValidityFilter } from './types';
import useFilterTrainSchedules from './useFilterTrainSchedules';
import { timetableHasInvalidTrain } from './utils';
import type { TimetableFilters, TimetableItemResult } from './types';
import { timetableHasInvalidItem } from './utils';

type TimetableToolbarProps = {
showTrainDetails: boolean;
toggleShowTrainDetails: () => void;
timetableItems: TimetableItemResult[];
displayedTimetableItems: TimetableItemResult[];
setDisplayedTimetableItems: (trainSchedulesDetails: TimetableItemResult[]) => void;
filteredTimetableItems: TimetableItemResult[];
timetableFilters: TimetableFilters;
selectedTimetableItemIds: TimetableItemId[];
setSelectedTimetableItemIds: (selectedTimetableIds: TimetableItemId[]) => void;
removeTrains: (trainIds: TimetableItemId[]) => void;
Expand All @@ -45,8 +43,8 @@ const TimetableToolbar = ({
showTrainDetails,
toggleShowTrainDetails,
timetableItems,
displayedTimetableItems,
setDisplayedTimetableItems,
filteredTimetableItems,
timetableFilters,
selectedTimetableItemIds,
setSelectedTimetableItemIds,
removeTrains,
Expand All @@ -61,13 +59,6 @@ const TimetableToolbar = ({

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

const [filter, setFilter] = useState('');
const [rollingStockFilter, setRollingStockFilter] = useState('');
const [validityFilter, setValidityFilter] = useState<ValidityFilter>('both');
const [scheduledPointsHonoredFilter, setScheduledPointsHonoredFilter] =
useState<ScheduledPointsHonoredFilter>('both');
const [selectedTags, setSelectedTags] = useState<Set<string | null>>(new Set());

const { selectedTrainScheduleIds, selectedPacedTrainIds } = useMemo(
() =>
selectedTimetableItemIds.reduce(
Expand Down Expand Up @@ -103,32 +94,17 @@ const TimetableToolbar = ({
[timetableItems]
);

const debouncedFilter = useDebounce(filter, 500);

const debouncedRollingstockFilter = useDebounce(rollingStockFilter, 500);

const [deleteTrainSchedules] = osrdEditoastApi.endpoints.deleteTrainSchedule.useMutation();

// TODO: move this hook in Timetable
const { uniqueTags } = useFilterTrainSchedules(
timetableItems,
debouncedFilter,
debouncedRollingstockFilter,
validityFilter,
scheduledPointsHonoredFilter,
selectedTags,
setDisplayedTimetableItems
);

const toggleFilterPanel = () => {
setIsFilterPanelOpen(!isFilterPanelOpen);
};

const toggleAllTrainsSelecton = () => {
if (displayedTimetableItems.length === selectedTimetableItemIds.length) {
if (filteredTimetableItems.length === selectedTimetableItemIds.length) {
setSelectedTimetableItemIds([]);
} else {
const timetableItemsDisplayed = displayedTimetableItems.map(({ id }) => id);
const timetableItemsDisplayed = filteredTimetableItems.map(({ id }) => id);
setSelectedTimetableItemIds(timetableItemsDisplayed);
}
};
Expand Down Expand Up @@ -303,7 +279,7 @@ const TimetableToolbar = ({
</div>
)}
</div>
{timetableHasInvalidTrain(displayedTimetableItems) && (
{timetableHasInvalidItem(filteredTimetableItems) && (
<div className="invalid-trains">
<Alert size="sm" variant="fill" />
<span data-testid="invalid-trains-message" className="invalid-trains-message">
Expand Down Expand Up @@ -332,17 +308,7 @@ const TimetableToolbar = ({
) : (
<FilterPanel
toggleFilterPanel={toggleFilterPanel}
filter={filter}
setFilter={setFilter}
rollingStockFilter={rollingStockFilter}
setRollingStockFilter={setRollingStockFilter}
validityFilter={validityFilter}
setValidityFilter={setValidityFilter}
scheduledPointsHonoredFilter={scheduledPointsHonoredFilter}
setScheduledPointsHonoredFilter={setScheduledPointsHonoredFilter}
uniqueTags={uniqueTags}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
timetableFilters={timetableFilters}
/>
)}
</div>
Expand Down
16 changes: 16 additions & 0 deletions front/src/modules/trainschedule/components/Timetable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,19 @@ export type PacedTrainWithResult = TimetableItemWithDetails & {
};

export type TimetableItemResult = TrainScheduleWithDetails | PacedTrainWithResult;

export type TimetableFilters = {
uniqueTags: string[];
nameLabelFilter: string;
setNameLabelFilter: (nameLabelFilter: string) => void;
rollingStockFilter: string;
setRollingStockFilter: (rollingStockFilter: string) => void;
validityFilter: ValidityFilter;
setValidityFilter: (validityFilter: ValidityFilter) => void;
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter;
setScheduledPointsHonoredFilter: (
scheduledPointsHonoredFilter: ScheduledPointsHonoredFilter
) => void;
selectedTags: Set<string | null>;
setSelectedTags: React.Dispatch<React.SetStateAction<Set<string | null>>>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useMemo, useState } from 'react';

import { uniq } from 'lodash';

import { useDebounce } from 'utils/helpers';

import type {
ScheduledPointsHonoredFilter,
TimetableFilters,
TimetableItemResult,
ValidityFilter,
} from './types';
import { extractTagCode, keepItem } from './utils';

/**
* Hook filtering a timetable items array depending on some filters
* @param timetableItems the timetable's items
* @returns all filters, their setters, the unique speed limit tags among all items and the filtered timetable items
*/
const useFilterTimetableItems = (
timetableItems: TimetableItemResult[]
): TimetableFilters & { filteredTimetableItems: TimetableItemResult[] } => {
const [nameLabelFilter, setNameLabelFilter] = useState('');
const [rollingStockFilter, setRollingStockFilter] = useState('');
const [validityFilter, setValidityFilter] = useState<ValidityFilter>('both');
const [scheduledPointsHonoredFilter, setScheduledPointsHonoredFilter] =
useState<ScheduledPointsHonoredFilter>('both');
const [selectedTags, setSelectedTags] = useState<Set<string | null>>(new Set());

const debouncedNameLabelFilter = useDebounce(nameLabelFilter, 500);
const debouncedRollingstockFilter = useDebounce(rollingStockFilter, 500);

const uniqueTags = useMemo(
() => uniq(timetableItems.map((timetableItem) => extractTagCode(timetableItem.speedLimitTag))),
[timetableItems]
);

const filteredTimetableItems: TimetableItemResult[] = useMemo(
() =>
timetableItems.filter((timetableItem) => {
if (!keepItem(timetableItem, debouncedNameLabelFilter)) return false;

// Apply validity filter
if (validityFilter !== 'both') {
if (validityFilter === 'valid' && !timetableItem.isValid) return false;
if (validityFilter === 'invalid' && timetableItem.isValid) return false;
}

// Apply scheduled points honored filter
if (scheduledPointsHonoredFilter !== 'both') {
if (!timetableItem.isValid) {
return false;
}
const { scheduledPointsNotHonored } = timetableItem;
if (
(scheduledPointsHonoredFilter === 'honored' && scheduledPointsNotHonored) ||
(scheduledPointsHonoredFilter === 'notHonored' && !scheduledPointsNotHonored)
) {
return false;
}
}

// Apply tag filter
if (
selectedTags.size > 0 &&
!selectedTags.has(extractTagCode(timetableItem.speedLimitTag))
) {
return false;
}

// Apply rolling stock filter
if (debouncedRollingstockFilter) {
const {
detail = '',
family = '',
reference = '',
series = '',
subseries = '',
} = timetableItem.rollingStock?.metadata || {};
if (
![detail, family, reference, series, subseries].some((v) =>
v.toLowerCase().includes(debouncedRollingstockFilter.toLowerCase())
)
)
return false;
}

return true;
}),
[
timetableItems,
debouncedNameLabelFilter,
debouncedRollingstockFilter,
validityFilter,
scheduledPointsHonoredFilter,
selectedTags,
]
);

return {
filteredTimetableItems,
uniqueTags,
nameLabelFilter,
setNameLabelFilter,
rollingStockFilter,
setRollingStockFilter,
validityFilter,
setValidityFilter,
scheduledPointsHonoredFilter,
setScheduledPointsHonoredFilter,
selectedTags,
setSelectedTags,
};
};

export default useFilterTimetableItems;
Loading

0 comments on commit 3176332

Please sign in to comment.