Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refacto timetable filters #10958

Merged
merged 1 commit into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading