diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 714ac7e726..b11d2e918b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "6.0.1", - "@mtes-mct/monitor-ui": "13.7.4", + "@mtes-mct/monitor-ui": "13.8.0", "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", @@ -2504,9 +2504,9 @@ "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, "node_modules/@mtes-mct/monitor-ui": { - "version": "13.7.4", - "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-13.7.4.tgz", - "integrity": "sha512-kJeZiQJFLwm8K9cyu6pC3EMEgPYQxPcQX1au6LqARdIGiGQiK8SFccLKEUBMdTyhfa8zgqiizG1w89s116s8fw==", + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-13.8.0.tgz", + "integrity": "sha512-UJEfsJzscq4uAKZBRvLFBhcKD5VOVvBqtFDgW0mM2STxwB5rlLwqgtcRGdZE2rCWYm1g3+fKNzvRE07U0R3kJQ==", "dependencies": { "@babel/runtime": "7.22.15", "@tanstack/react-table": "8.9.7", diff --git a/frontend/package.json b/frontend/package.json index 90a34c45ee..99649b7378 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,7 +35,7 @@ "dependencies": { "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "6.0.1", - "@mtes-mct/monitor-ui": "13.7.4", + "@mtes-mct/monitor-ui": "13.8.0", "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx index 87c85e40b6..4e8d79f0b0 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx @@ -10,13 +10,13 @@ import { DateRangePicker, Icon, MultiCascader, - MultiSelect, RichBoolean, RichBooleanCheckbox, Select, Size, TextInput, - type DateAsStringRange + type DateAsStringRange, + CheckPicker } from '@mtes-mct/monitor-ui' import { assertNotNullish } from '@utils/assertNotNullish' import { useCallback } from 'react' @@ -38,13 +38,13 @@ export type FilterBarProps = { } export function FilterBar() { const listFilterValues = useMainAppSelector(store => store.priorNotification.listFilterValues) + const dispatch = useMainAppDispatch() const { fleetSegmentsAsOptions } = useGetFleetSegmentsAsOptions() const { gearsAsTreeOptions } = useGetGearsAsTreeOptions() const { portsAsTreeOptions } = useGetPortsAsTreeOptions() const { speciesAsOptions } = useGetSpeciesAsOptions() const { priorNotificationTypesAsOptions } = useGetPriorNotificationTypesAsOptions() - const dispatch = useMainAppDispatch() const updateCountryCodes = (nextCountryCodes: string[] | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ countryCodes: nextCountryCodes })) @@ -123,20 +123,23 @@ export function FilterBar() { - + items.length > 0 ? Nationalités ({items.length}) : <> + } searchable value={listFilterValues.countryCodes} virtualized /> - + items.length > 0 ? Segments de flotte ({items.length}) : <> + } searchable value={listFilterValues.fleetSegmentSegments} virtualized /> - + items.length > 0 ? Espèces à bord ({items.length}) : <> + } searchable value={listFilterValues.specyCodes} virtualized @@ -174,6 +183,9 @@ export function FilterBar() { options={gearsAsTreeOptions ?? []} placeholder="Engins utilisés" popupWidth={500} + renderValue={(_, items) => + items.length > 0 ? Engins utilisés ({items.length}) : <> + } searchable value={listFilterValues.gearCodes} /> @@ -225,10 +237,13 @@ export function FilterBar() { options={portsAsTreeOptions ?? []} placeholder="Ports d’arrivée" popupWidth={500} + renderValue={(_, items) => + items.length > 0 ? Ports d’arrivée ({items.length}) : <> + } searchable value={listFilterValues.portLocodes} /> - + items.length > 0 ? Types de préavis ({items.length}) : <> + } searchable value={listFilterValues.priorNotificationTypes} virtualized @@ -304,3 +322,11 @@ const Row = styled.div` min-width: 320px; } ` + +const SelectValue = styled.span` + display: flex; + overflow: hidden; + pointer-events: none; + text-overflow: ellipsis; + white-space: nowrap; +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterTags.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterTags.tsx new file mode 100644 index 0000000000..8fdf16e6cc --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterTags.tsx @@ -0,0 +1,136 @@ +import { COUNTRIES_AS_ALPHA3_OPTIONS } from '@constants/index' +import { useGetPriorNotificationTypesAsOptions } from '@features/PriorNotification/hooks/useGetPriorNotificationTypesAsOptions' +import { priorNotificationActions } from '@features/PriorNotification/slice' +import { useGetFleetSegmentsAsOptions } from '@hooks/useGetFleetSegmentsAsOptions' +import { useGetGearsAsTreeOptions } from '@hooks/useGetGearsAsTreeOptions' +import { useGetPortsAsTreeOptions } from '@hooks/useGetPortsAsTreeOptions' +import { useGetSpeciesAsOptions } from '@hooks/useGetSpeciesAsOptions' +import { useMainAppDispatch } from '@hooks/useMainAppDispatch' +import { useMainAppSelector } from '@hooks/useMainAppSelector' +import { SingleTag, getSelectedOptionFromOptionValueInTree } from '@mtes-mct/monitor-ui' +import styled from 'styled-components' + +import type { ListFilter } from './types' + +export function FilterTags() { + const listFilterValues = useMainAppSelector(store => store.priorNotification.listFilterValues) + const dispatch = useMainAppDispatch() + + const { fleetSegmentsAsOptions } = useGetFleetSegmentsAsOptions() + const { gearsAsTreeOptions } = useGetGearsAsTreeOptions() + const { portsAsTreeOptions } = useGetPortsAsTreeOptions() + const { speciesAsOptions } = useGetSpeciesAsOptions() + const { priorNotificationTypesAsOptions } = useGetPriorNotificationTypesAsOptions() + + const remove = (key: keyof ListFilter, value: string) => { + const filterValue = listFilterValues[key] + + if (!filterValue) { + throw new Error('`filterValue` is undefined.') + } + + const nextFilterValue = Array.isArray(filterValue) ? filterValue.filter(v => v !== value) : undefined + const normalizedNextFilterValue = + Array.isArray(nextFilterValue) && !nextFilterValue.length ? undefined : nextFilterValue + const nextListFilterValues = { ...listFilterValues, [key]: normalizedNextFilterValue } + + dispatch(priorNotificationActions.setListFilterValues(nextListFilterValues)) + } + + const removeAll = () => { + dispatch(priorNotificationActions.resetListFilterValues()) + } + + return ( + + + {!!listFilterValues.countryCodes && + listFilterValues.countryCodes.map(countryCode => ( + remove('countryCodes', countryCode)}> + {String(COUNTRIES_AS_ALPHA3_OPTIONS.find(option => option.value === countryCode)?.label)} + + ))} + + {!!listFilterValues.fleetSegmentSegments && + !!fleetSegmentsAsOptions && + listFilterValues.fleetSegmentSegments.map(fleetSegmentSegment => ( + remove('fleetSegmentSegments', fleetSegmentSegment)} + > + {String(fleetSegmentsAsOptions.find(option => option.value === fleetSegmentSegment)?.label)} + + ))} + + {!!listFilterValues.specyCodes && + !!speciesAsOptions && + listFilterValues.specyCodes.map(specyCode => ( + remove('specyCodes', specyCode)}> + {String(speciesAsOptions.find(option => option.value === specyCode)?.label)} + + ))} + + {!!listFilterValues.gearCodes && + !!gearsAsTreeOptions && + listFilterValues.gearCodes.map(gearCode => ( + remove('gearCodes', gearCode)}> + {getSelectedOptionFromOptionValueInTree(gearsAsTreeOptions, gearCode)?.label} + + ))} + + {!!listFilterValues.portLocodes && + !!portsAsTreeOptions && + listFilterValues.portLocodes.map(portLocode => ( + remove('portLocodes', portLocode)}> + {getSelectedOptionFromOptionValueInTree(portsAsTreeOptions, portLocode)?.label} + + ))} + + {!!listFilterValues.priorNotificationTypes && + !!priorNotificationTypesAsOptions && + listFilterValues.priorNotificationTypes.map(priorNotificationType => ( + remove('priorNotificationTypes', priorNotificationType)} + > + {String(priorNotificationTypesAsOptions.find(option => option.value === priorNotificationType)?.label)} + + ))} + + + + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + Réinitialiser les filtres + + + ) +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + flex-wrap: wrap; + margin-bottom: 24px; + + > div:not(:first-child) { + margin-top: 12px; + } +` + +const Row = styled.div` + align-items: center; + display: flex; + flex-wrap: wrap; + + > .Component-SingleTag { + margin: 0 8px 8px 0; + } +` + +const Link = styled.a` + align-items: center; + color: ${p => p.theme.color.charcoal}; + cursor: pointer; + line-height: 1; + text-decoration: underline; +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx index be4a2e8457..35cedd6f09 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx @@ -19,6 +19,7 @@ import styled from 'styled-components' import { PRIOR_NOTIFICATION_TABLE_COLUMNS, SUB_MENUS_AS_OPTIONS } from './constants' import { FilterBar } from './FilterBar' +import { FilterTags } from './FilterTags' import { countPriorNotificationsForSeaFrontGroup, getApiFilterFromListFilter, @@ -121,6 +122,7 @@ export function PriorNotificationList() { + {isError &&
Une erreur est survenue.
} diff --git a/frontend/src/features/PriorNotification/constants.ts b/frontend/src/features/PriorNotification/constants.ts new file mode 100644 index 0000000000..4bad75e6b8 --- /dev/null +++ b/frontend/src/features/PriorNotification/constants.ts @@ -0,0 +1,23 @@ +import { RichBoolean } from '@mtes-mct/monitor-ui' + +import { ExpectedArrivalPeriod } from './components/PriorNotificationList/constants' +import { SeaFrontGroup } from '../../domain/entities/seaFront/constants' + +import type { ListFilter } from './components/PriorNotificationList/types' + +export const DEFAULT_LIST_FILTER_VALUES: ListFilter = { + countryCodes: undefined, + expectedArrivalCustomPeriod: undefined, + expectedArrivalPeriod: ExpectedArrivalPeriod.IN_LESS_THAN_FOUR_HOURS, + fleetSegmentSegments: undefined, + gearCodes: undefined, + hasOneOrMoreReportings: RichBoolean.BOTH, + isLessThanTwelveMetersVessel: RichBoolean.BOTH, + isSent: undefined, + lastControlPeriod: undefined, + portLocodes: undefined, + priorNotificationTypes: undefined, + seaFrontGroup: SeaFrontGroup.ALL, + searchQuery: undefined, + specyCodes: undefined +} diff --git a/frontend/src/features/PriorNotification/slice.ts b/frontend/src/features/PriorNotification/slice.ts index ac406c731c..84b43accbf 100644 --- a/frontend/src/features/PriorNotification/slice.ts +++ b/frontend/src/features/PriorNotification/slice.ts @@ -1,8 +1,6 @@ -import { RichBoolean } from '@mtes-mct/monitor-ui' import { createSlice, type PayloadAction } from '@reduxjs/toolkit' -import { ExpectedArrivalPeriod } from './components/PriorNotificationList/constants' -import { SeaFrontGroup } from '../../domain/entities/seaFront/constants' +import { DEFAULT_LIST_FILTER_VALUES } from './constants' import type { ListFilter } from './components/PriorNotificationList/types' @@ -10,28 +8,17 @@ interface PriorNotificationState { listFilterValues: ListFilter } const INITIAL_STATE: PriorNotificationState = { - listFilterValues: { - countryCodes: undefined, - expectedArrivalCustomPeriod: undefined, - expectedArrivalPeriod: ExpectedArrivalPeriod.IN_LESS_THAN_FOUR_HOURS, - fleetSegmentSegments: undefined, - gearCodes: undefined, - hasOneOrMoreReportings: RichBoolean.BOTH, - isLessThanTwelveMetersVessel: RichBoolean.BOTH, - isSent: undefined, - lastControlPeriod: undefined, - portLocodes: undefined, - priorNotificationTypes: undefined, - seaFrontGroup: SeaFrontGroup.ALL, - searchQuery: undefined, - specyCodes: undefined - } + listFilterValues: DEFAULT_LIST_FILTER_VALUES } const priorNotificationSlice = createSlice({ initialState: INITIAL_STATE, name: 'priorNotification', reducers: { + resetListFilterValues(state) { + state.listFilterValues = DEFAULT_LIST_FILTER_VALUES + }, + setListFilterValues(state, action: PayloadAction>) { state.listFilterValues = { ...state.listFilterValues, diff --git a/frontend/src/features/SideWindow/MissionList/FilterBar.tsx b/frontend/src/features/SideWindow/MissionList/FilterBar.tsx index 6703c4c5fc..b732497fa9 100644 --- a/frontend/src/features/SideWindow/MissionList/FilterBar.tsx +++ b/frontend/src/features/SideWindow/MissionList/FilterBar.tsx @@ -28,10 +28,10 @@ import { missionListActions } from '../../Mission/components/MissionList/slice' import type { FilterValues } from './types' import type { Promisable } from 'type-fest' -export type FilterBarProps = { +export type FilterBarProps = Readonly<{ onQueryChange: (nextQuery: string | undefined) => Promisable searchQuery: string | undefined -} +}> export function FilterBar({ onQueryChange, searchQuery }: FilterBarProps) { const listFilterValues = useMainAppSelector(store => store.missionList.listFilterValues)