Skip to content

Commit

Permalink
[Unité] Ajout des bases sur la carte (#2726)
Browse files Browse the repository at this point in the history
## Linked issues

- MTES-MCT/monitorenv#698

----

- [x] Tests E2E (Cypress)
  • Loading branch information
louptheron authored Dec 4, 2023
2 parents dd9259d + 85adf92 commit 0a03cf8
Show file tree
Hide file tree
Showing 60 changed files with 1,296 additions and 142 deletions.
2 changes: 1 addition & 1 deletion frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ module.exports = {

// Jest
{
files: ['**/*.test.ts', '**/*.test.tsx'],
files: ['__mocks__/**/*.[j|t]s', '**/*.test.ts', '**/*.test.tsx'],
plugins: ['jest'],
env: {
jest: true
Expand Down
6 changes: 6 additions & 0 deletions frontend/__mocks__/ol/Map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class MockedMap {}
MockedMap.prototype.getCoordinateFromPixel = jest.fn()
MockedMap.prototype.getPixelFromCoordinate = jest.fn()

module.exports = MockedMap
module.exports.default = MockedMap
18 changes: 18 additions & 0 deletions frontend/cypress/e2e/main_window/station_overlay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
context('Main Window > Station Overlay', () => {
beforeEach(() => {
cy.visit(`/#@-282045.85,6101658.31,9.11`).wait(5000)

cy.clickButton('Liste des unités de contrôle')
cy.clickButton('Afficher les bases').wait(1000)
})

it('Should show the expected station card when selected', () => {
// Click on Vannes base
cy.get('.ol-viewport').click(550, 650, { force: true })

cy.getDataCy('StationOverlay-card').contains('Vannes').should('be.visible')
cy.getDataCy('StationOverlay-card').contains('Cultures marines 56').should('be.visible')
cy.getDataCy('StationOverlay-card').find('.Element-Tag').eq(0).should('have.text', '1 Vedette')
cy.getDataCy('StationOverlay-card').find('.Element-Tag').eq(1).should('have.text', '1 Voiture')
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/map-icons/station-layer-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/api/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FIVE_MINUTES } from '../constants'

export const RTK_COMMON_QUERY_OPTIONS = {
export const RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS = {
pollingInterval: FIVE_MINUTES
}

Expand Down
8 changes: 8 additions & 0 deletions frontend/src/domain/entities/layers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export enum LayerType {
MISSION = 'MISSION',
REGULATORY = 'REGULATORY',
REGULATORY_PREVIEW = 'REGULATORY_PREVIEW',
STATION = 'STATION',
VESSEL = 'VESSEL',
VESSEL_ALERT = 'VESSEL_ALERT',
VESSEL_ALERT_AND_BEACON_MALFUNCTION = 'VESSEL_ALERT_AND_BEACON_MALFUNCTION',
Expand Down Expand Up @@ -100,6 +101,13 @@ export const LayerProperties: Record<MonitorFishLayer, ShowableLayer> = {
type: LayerType.DRAW,
zIndex: 999
},
[MonitorFishLayer.STATION]: {
code: MonitorFishLayer.STATION,
type: LayerType.STATION,
zIndex: 1001,
isClickable: true,
isHoverable: true
},
[MonitorFishLayer.FILTERED_VESSELS]: {
code: MonitorFishLayer.FILTERED_VESSELS,
type: LayerType.VESSEL,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/domain/entities/layers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum MonitorFishLayer {
SELECTED_VESSEL = 'SELECTED_VESSEL',
SIOFA = 'SIOFA',
SIX_MILES = 'SIX_MILES',
STATION = 'STATION',
THREE_MILES = 'THREE_MILES',
TWELVE_MILES = 'TWELVE_MILES',
VESSELS = 'VESSELS_POINTS',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/domain/shared_slices/DisplayedComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type DisplayedComponentState = {
isMeasurementMapButtonDisplayed: boolean
isMissionsLayerDisplayed: boolean
isMissionsMapButtonDisplayed: boolean
isStationLayerDisplayed: boolean
isVesselFiltersMapButtonDisplayed: boolean
isVesselLabelsMapButtonDisplayed: boolean
isVesselListDisplayed: boolean
Expand All @@ -42,6 +43,7 @@ const INITIAL_STATE: DisplayedComponentState = {
'isMissionsLayerDisplayed'
),
isMissionsMapButtonDisplayed: true,
isStationLayerDisplayed: false,
isVesselFiltersMapButtonDisplayed: true,
isVesselLabelsMapButtonDisplayed: true,
isVesselListDisplayed: true,
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/domain/shared_slices/Global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ export const globalSlice = createSlice({
}
})

export const globalActions = globalSlice.actions
export const globalSliceReducer = globalSlice.reducer

export const {
addSearchedVessel,
closeVesselListModal,
Expand All @@ -191,5 +194,3 @@ export const {
setPreviewFilteredVesselsMode,
setUserType
} = globalSlice.actions

export const globalSliceReducer = globalSlice.reducer
7 changes: 4 additions & 3 deletions frontend/src/domain/shared_slices/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ const mapSlice = createSlice({
}
})

export const mapActions = mapSlice.actions
export const mapReducer = mapSlice.reducer

export const {
animateToCoordinates,
animateToExtent,
Expand All @@ -206,6 +209,4 @@ export const {
setVesselLabelsShowedOnMap,
setVesselsLastPositionVisibility,
showVesselsEstimatedPositions
} = mapSlice.actions

export const mapReducer = mapSlice.reducer
} = mapActions
3 changes: 2 additions & 1 deletion frontend/src/domain/types/map.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { FeatureWithCodeAndEntityId } from '../../libs/FeatureWithCodeAndEntityId'
import type { InteractionListener, InteractionType } from '../entities/map/constants'
import type { FeatureLike } from 'ol/Feature'

export type MapClick = {
ctrlKeyPressed: boolean
feature: FeatureLike | undefined
feature: FeatureLike | FeatureWithCodeAndEntityId | undefined
}

export type LastPositionVisibility = {
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/domain/use_cases/map/clickOnMapFeature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import GeoJSON from 'ol/format/GeoJSON'

import { showRegulatoryZoneMetadata } from '../../../features/Regulation/useCases/showRegulatoryZoneMetadata'
import {
FEATURE_MARGINS,
STATION_OVERLAY_DIALOG_WIDTH_AND_HEIGHT
} from '../../../features/Station/components/SelectedStationOverlay/constants'
import { stationActions } from '../../../features/Station/slice'
import { FeatureWithCodeAndEntityId } from '../../../libs/FeatureWithCodeAndEntityId'
import { getDialogOverlayPositionFromFeature } from '../../../utils/getDialogOverlayPositionFromFeature'
import { missionActions } from '../../actions'
import { isControl } from '../../entities/controls'
import { LayerProperties } from '../../entities/layers/constants'
Expand Down Expand Up @@ -62,6 +69,23 @@ export const clickOnMapFeature = (mapClick: MapClick) => (dispatch, getState) =>
return
}

if (mapClick.feature instanceof FeatureWithCodeAndEntityId && mapClick.feature.code === MonitorFishLayer.STATION) {
const overlayPosition = getDialogOverlayPositionFromFeature(
mapClick.feature,
STATION_OVERLAY_DIALOG_WIDTH_AND_HEIGHT,
FEATURE_MARGINS
)

dispatch(
stationActions.selectStation({
overlayPosition,
stationId: mapClick.feature.entityId
})
)

return
}

if (previewFilteredVesselsMode) {
return
}
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/domain/use_cases/vessel/showVessel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { logbookActions } from '../../../features/Logbook/slice'
import { addVesselIdentifierToVesselIdentity } from '../../../features/VesselSearch/utils'
import { Vessel } from '../../entities/vessel/vessel'
import { getCustomOrDefaultTrackRequest, throwCustomErrorFromAPIFeedback } from '../../entities/vesselTrackDepth'
import { displayedComponentActions } from '../../shared_slices/DisplayedComponent'
import { setDisplayedErrors } from '../../shared_slices/DisplayedError'
import { addSearchedVessel, removeError, setError } from '../../shared_slices/Global'
import { doNotAnimate } from '../../shared_slices/Map'
Expand All @@ -21,6 +22,13 @@ export const showVessel =
const { selectedVesselTrackRequest, vessels } = vessel
const { defaultVesselTrackDepth } = map
const { areFishingActivitiesShowedOnMap } = fishingActivities
// TODO How to handle both the control unit dialog and the vessel sidebar ?
dispatch(
displayedComponentActions.setDisplayedComponents({
isControlUnitDialogDisplayed: false,
isControlUnitListDialogDisplayed: false
})
)

const vesselFeatureId = Vessel.getVesselFeatureId(vesselIdentity)
const selectedVesselLastPosition = vessels.find(lastPosition => lastPosition.vesselFeatureId === vesselFeatureId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { Formik } from 'formik'
import styled from 'styled-components'

import { CONTROL_UNIT_RESOURCE_FORM_SCHEMA, CONTROL_UNIT_RESOURCE_TYPES_AS_OPTIONS } from './constants'
import { RTK_COMMON_QUERY_OPTIONS } from '../../../../../api/constants'
import { useGetStationsQuery } from '../../../../../api/station'
import { RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS } from '../../../../../api/constants'
import { useGetStationsQuery } from '../../../../Station/stationApi'

import type { ControlUnitResourceFormValues } from './types'
import type { CSSProperties } from 'react'
Expand All @@ -34,7 +34,7 @@ export function Form({ className, initialValues, onArchive, onCancel, onDelete,
const key = useKey([initialValues])
const isNew = !initialValues.id

const { data: stations } = useGetStationsQuery(undefined, RTK_COMMON_QUERY_OPTIONS)
const { data: stations } = useGetStationsQuery(undefined, RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS)

const stationsAsOptions = getOptionsFromIdAndName(stations)?.filter(stationAsOption => stationAsOption.value !== 0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import styled from 'styled-components'
import { AreaNote } from './AreaNote'
import { ControlUnitContactList } from './ControlUnitContactList'
import { ControlUnitResourceList } from './ControlUnitResourceList'
import { RTK_COMMON_QUERY_OPTIONS } from '../../../../api/constants'
import { RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS } from '../../../../api/constants'
import { displayedComponentActions } from '../../../../domain/shared_slices/DisplayedComponent'
import { useMainAppDispatch } from '../../../../hooks/useMainAppDispatch'
import { useMainAppSelector } from '../../../../hooks/useMainAppSelector'
Expand All @@ -26,7 +26,7 @@ export function ControlUnitDialog() {

const { data: controlUnit, error: getControlControlUnitError } = useGetControlUnitQuery(
controlUnitId,
RTK_COMMON_QUERY_OPTIONS
RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS
)
FrontendApiError.handleIfAny(getControlControlUnitError)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,26 @@ import { useCallback, useMemo } from 'react'
import styled from 'styled-components'

import { controlUnitListDialogActions } from './slice'
import { RTK_COMMON_QUERY_OPTIONS } from '../../../../api/constants'
import { useGetStationsQuery } from '../../../../api/station'
import { RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS } from '../../../../api/constants'
import { useMainAppDispatch } from '../../../../hooks/useMainAppDispatch'
import { useMainAppSelector } from '../../../../hooks/useMainAppSelector'
import { FrontendApiError } from '../../../../libs/FrontendApiError'
import { isNotArchived } from '../../../../utils/isNotArchived'
import { useGetStationsQuery } from '../../../Station/stationApi'
import { useGetAdministrationsQuery } from '../../administrationApi'

export function FilterBar() {
const dispatch = useMainAppDispatch()
const filtersState = useMainAppSelector(store => store.controlUnitListDialog.filtersState)
const { data: administrations, error: getAdministrationsError } = useGetAdministrationsQuery(
undefined,
RTK_COMMON_QUERY_OPTIONS
RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS
)
FrontendApiError.handleIfAny(getAdministrationsError)
const { data: bases, error: getStationsError } = useGetStationsQuery(undefined, RTK_COMMON_QUERY_OPTIONS)
const { data: bases, error: getStationsError } = useGetStationsQuery(
undefined,
RTK_FIVE_MINUTES_POLLING_QUERY_OPTIONS
)
FrontendApiError.handleIfAny(getStationsError)

const administrationsAsOptions = useMemo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,61 @@
import { IconButton, type ControlUnit, Accent, Icon } from '@mtes-mct/monitor-ui'
import { property, uniqBy } from 'lodash/fp'
import { fromLonLat } from 'ol/proj'
import styled from 'styled-components'

import { displayControlUnitResourcesFromControlUnit, displayBaseNamesFromControlUnit } from './utils'
import {
displayControlUnitResourcesFromControlUnit,
displayBaseNamesFromControlUnit,
getBufferedExtentFromStations
} from './utils'
import { displayedComponentActions } from '../../../../domain/shared_slices/DisplayedComponent'
import { mapActions } from '../../../../domain/shared_slices/Map'
import { useMainAppDispatch } from '../../../../hooks/useMainAppDispatch'
import { useMainAppSelector } from '../../../../hooks/useMainAppSelector'
import { FrontendError } from '../../../../libs/FrontendError'
import { monitorfishMap } from '../../../map/monitorfishMap'
import { stationActions } from '../../../Station/slice'
import { controlUnitDialogActions } from '../ControlUnitDialog/slice'

import type { ControlUnit } from '@mtes-mct/monitor-ui'
const FIVE_SECONDS = 5000

export type ItemProps = {
controlUnit: ControlUnit.ControlUnit
}
export function Item({ controlUnit }: ItemProps) {
const dispatch = useMainAppDispatch()
const isStationLayerDisplayed = useMainAppSelector(state => state.displayedComponent.isStationLayerDisplayed)

const center = () => {
const highlightedStations = uniqBy(
property('id'),
controlUnit.controlUnitResources.map(({ station }) => station)
)

const highlightedStationIds = highlightedStations.map(station => station.id)

if (highlightedStations.length === 1) {
const station = highlightedStations[0]
if (!station) {
throw new FrontendError('`station` is undefined.')
}

const stationCoordinates = fromLonLat([station.longitude, station.latitude])

// Add this as a `monitorfishMap` method (vanilla).
monitorfishMap.getView().animate({ center: stationCoordinates })
} else {
const bufferedHighlightedStationsExtent = getBufferedExtentFromStations(highlightedStations, 0.5)

// Move this indirect method to `monitorfishMap` (vanilla).
dispatch(mapActions.fitToExtent(bufferedHighlightedStationsExtent))
}

dispatch(stationActions.highlightStationIds(highlightedStationIds))
setTimeout(() => {
dispatch(stationActions.highlightStationIds([]))
}, FIVE_SECONDS)
}

const edit = () => {
dispatch(controlUnitDialogActions.setControlUnitId(controlUnit.id))
Expand All @@ -27,6 +71,18 @@ export function Item({ controlUnit }: ItemProps) {
<Wrapper data-cy="ControlUnitListDialog-control-unit" data-id={controlUnit.id} onClick={edit}>
<Head>
<NameText>{controlUnit.name}</NameText>

{isStationLayerDisplayed && (
<IconButton
accent={Accent.TERTIARY}
disabled={!controlUnit.controlUnitResources.length}
Icon={Icon.FocusZones}
iconSize={18}
isCompact
onClick={center}
withUnpropagatedClick
/>
)}
</Head>
<AdministrationText>{controlUnit.administration.name}</AdministrationText>
<ResourcesAndPortsText>{displayControlUnitResourcesFromControlUnit(controlUnit)}</ResourcesAndPortsText>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from '@jest/globals'
import { ControlUnit } from '@mtes-mct/monitor-ui'

import { displayControlUnitResourcesFromControlUnit } from '../utils'

describe('features/ControlUnit/components/ControlUnitListDialog/utils > displayControlUnitResourcesFromControlUnit()', () => {
it('should return formatted resource string for multiple resources', () => {
const controlUnit = {
controlUnitResources: [
{ isArchived: false, type: ControlUnit.ControlUnitResourceType.CAR },
{ isArchived: false, type: ControlUnit.ControlUnitResourceType.CAR },
{ isArchived: false, type: ControlUnit.ControlUnitResourceType.DRONE }
]
} as unknown as ControlUnit.ControlUnit

const result = displayControlUnitResourcesFromControlUnit(controlUnit)

expect(result).toEqual('2 Voitures, 1 Drône')
})

it('should handle empty resource list', () => {
const controlUnit = {
controlUnitResources: []
} as unknown as ControlUnit.ControlUnit

const result = displayControlUnitResourcesFromControlUnit(controlUnit)

expect(result).toEqual('Aucun moyen')
})

it('should not count archived resources', () => {
const controlUnit = {
controlUnitResources: [
{ isArchived: true, type: ControlUnit.ControlUnitResourceType.CAR },
{ isArchived: false, type: ControlUnit.ControlUnitResourceType.DRONE }
]
} as unknown as ControlUnit.ControlUnit

const result = displayControlUnitResourcesFromControlUnit(controlUnit)
expect(result).toEqual('1 Drône')
})
})
Loading

0 comments on commit 0a03cf8

Please sign in to comment.