Skip to content

Commit

Permalink
[Missions] Edition de la mission depuis la fiche navire (#2307)
Browse files Browse the repository at this point in the history
## Linked issues

- Resolve #2276 
- Affiche un message d'erreur et le retry au lieu du chargement infini
lorsque la requête pour chercher les missions ne trouve pas de mission :
![Screenshot from 2023-07-10
16-18-25](https://github.com/MTES-MCT/monitorfish/assets/14853556/fcc89648-25e4-4fdb-9a32-8034d7246f6f)

----

- [x] Tests E2E (Cypress)
  • Loading branch information
louptheron authored Jul 12, 2023
2 parents fe79d7d + f6f9578 commit 2ec6a29
Show file tree
Hide file tree
Showing 44 changed files with 352 additions and 200 deletions.
7 changes: 7 additions & 0 deletions frontend/cypress/e2e/external_monitorfish.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ context('External MonitorFish', () => {
cy.get('*[data-cy="vessel-menu-identity"]').should('be.visible')
cy.get('*[data-cy="vessel-menu-fishing"]').should('be.visible')
cy.get('*[data-cy="vessel-menu-controls"]').should('be.visible')

// Should not include the modify mission button
cy.get('*[data-cy="vessel-menu-controls"]').click()
cy.get('*[data-cy="vessel-controls"]', { timeout: 10000 }).should('be.visible')
cy.get('*[data-cy="vessel-controls-year"]').first().click({ timeout: 10000 })
cy.get('*[data-cy="vessel-control"]').should('not.contain', 'Modifier le CR du contrôle')

cy.get('*[data-cy="vessel-menu-resume"]').should('not.exist')
cy.get('*[data-cy="vessel-menu-reporting"]').should('not.exist')
cy.get('*[data-cy="vessel-menu-ers-vms"]').should('not.exist')
Expand Down
14 changes: 14 additions & 0 deletions frontend/cypress/e2e/side_window/mission_form/main_form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,4 +478,18 @@ context('Side Window > Mission Form > Main Form', () => {

cy.get('h1').should('contain.text', 'Missions et contrôles')
})

it('Should display an error message When a mission could not be fetched', () => {
cy.intercept(
{
method: 'GET',
path: '/api/v1/missions/6',
times: 1
},
{ statusCode: 400 }
).as('getMissionStubbed')
editSideWindowMissionListMissionWithId(6, SeaFrontGroup.MED)
cy.wait('@getMissionStubbed')
cy.get('*[data-cy="mission-form-error"]').contains("Nous n'avons pas pu récupérer la mission")
})
})
22 changes: 22 additions & 0 deletions frontend/cypress/e2e/vessel_sidebar/controls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,26 @@ context('Vessel sidebar controls tab', () => {
.eq(1)
.contains(`CONTRÔLE EN MER DU ${getDate(yearBeforeMinusOneMonth.toISOString())}`)
})

it('A control mission Should be opened in the side window', () => {
// Given
cy.get('.VESSELS_POINTS').click(460, 480, { force: true, timeout: 10000 })
cy.wait(200)
cy.get('*[data-cy="vessel-sidebar"]', { timeout: 10000 }).should('be.visible')

// When
cy.get('*[data-cy="vessel-menu-controls"]').click({ timeout: 10000 })
cy.get('*[data-cy="vessel-controls"]', { timeout: 10000 }).should('be.visible')
cy.get('*[data-cy="vessel-controls-year"]').first().click({ timeout: 10000 })

// Click on Modify mission button
cy.window().then(win => {
cy.stub(win, 'open', () => {
// eslint-disable-next-line no-param-reassign
win.location.href = '/side_window'
}).as('side_window')
})
cy.clickButton('Modifier le CR du contrôle')
cy.get('@side_window').should('be.called')
})
})
10 changes: 5 additions & 5 deletions frontend/src/api/APIWorker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export function APIWorker() {
}

vesselBeaconMalfunctionInterval.current = setInterval(() => {
dispatch(getVesselBeaconMalfunctions(true))
dispatch(getVesselBeaconMalfunctions(false))
}, THIRTY_SECONDS)
}

Expand All @@ -134,13 +134,13 @@ export function APIWorker() {
}

if (vesselSidebarTab === VesselSidebarTab.VOYAGES && selectedVesselIdentity) {
dispatch(getVesselLogbook(selectedVesselIdentity, undefined, true))
dispatch(getVesselLogbook(selectedVesselIdentity, undefined, false))
} else if (vesselSidebarTab === VesselSidebarTab.CONTROLS) {
dispatch(getVesselControls(true))
dispatch(getVesselControls(false))
} else if (vesselSidebarTab === VesselSidebarTab.REPORTING) {
dispatch(getVesselReportings(true))
dispatch(getVesselReportings(false))
} else if (isSuperUser && vesselSidebarTab === VesselSidebarTab.ERSVMS) {
dispatch(getVesselBeaconMalfunctions(true))
dispatch(getVesselBeaconMalfunctions(false))
}

setUpdateVesselSidebarTab(false)
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/api/mission.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { monitorenvApi, monitorfishApi } from '.'
import { ApiError } from '../libs/ApiError'

import type { Mission, MissionWithActions } from '../domain/entities/mission/types'

const GET_MISSION_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la mission"

export const monitorenvMissionApi = monitorenvApi.injectEndpoints({
endpoints: builder => ({
createMission: builder.mutation<Pick<Mission.Mission, 'id'>, Mission.MissionData>({
Expand Down Expand Up @@ -31,7 +34,8 @@ export const monitorenvMissionApi = monitorenvApi.injectEndpoints({

getMission: builder.query<Mission.Mission, Mission.Mission['id']>({
providesTags: [{ type: 'Missions' }],
query: id => `missions/${id}`
query: id => `missions/${id}`,
transformErrorResponse: response => new ApiError(GET_MISSION_ERROR_MESSAGE, response)
}),

updateMission: builder.mutation<void, Mission.Mission>({
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/api/missionAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ApiError } from '../libs/ApiError'

import type { MissionAction } from '../domain/types/missionAction'

const GET_MISSION_ACTIONS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer les actions de la mission"

export const missionActionApi = monitorfishApi.injectEndpoints({
endpoints: builder => ({
createMissionAction: builder.mutation<void, MissionAction.MissionActionData>({
Expand All @@ -24,7 +26,8 @@ export const missionActionApi = monitorfishApi.injectEndpoints({

getMissionActions: builder.query<MissionAction.MissionAction[], number>({
providesTags: () => [{ type: 'MissionActions' }],
query: missionId => `/mission_actions?missionId=${missionId}`
query: missionId => `/mission_actions?missionId=${missionId}`,
transformErrorResponse: response => new ApiError(GET_MISSION_ACTIONS_ERROR_MESSAGE, response)
}),

updateMissionAction: builder.mutation<void, MissionAction.MissionAction>({
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/domain/entities/vesselTrackDepth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,30 +73,30 @@ export const getTrackRequestFromDates = (afterDateTime: Date, beforeDateTime: Da
export function throwCustomErrorFromAPIFeedback(
positions: VesselPosition[],
isTrackDepthModified: boolean,
isCalledFromCron: boolean
isFromUserAction: boolean
) {
if (trackDepthHasBeenModifiedFromAPI(positions, isTrackDepthModified, isCalledFromCron)) {
if (trackDepthHasBeenModifiedFromAPI(positions, isTrackDepthModified, isFromUserAction)) {
throw new NoDEPFoundError(
"Nous n'avons pas trouvé de dernier DEP pour ce navire, nous affichons " +
'les positions des dernières 24 heures.'
)
}
if (noPositionsFoundForVessel(positions, isCalledFromCron)) {
if (noPositionsFoundForVessel(positions, isFromUserAction)) {
throw new NoPositionsFoundError("Nous n'avons trouvé aucune position.")
}
if (noPositionsFoundForEnteredDateTime(positions)) {
throw new NoPositionsFoundError("Nous n'avons trouvé aucune position pour ces dates.")
}
}

function noPositionsFoundForVessel(positions, updateShowedVessel) {
return !positions?.length && !updateShowedVessel
function noPositionsFoundForVessel(positions, isFromUserAction) {
return !positions?.length && isFromUserAction
}

function noPositionsFoundForEnteredDateTime(positions) {
return !positions?.length
}

function trackDepthHasBeenModifiedFromAPI(positions, isTrackDepthModified, updateShowedVessel) {
return positions?.length && isTrackDepthModified && !updateShowedVessel
function trackDepthHasBeenModifiedFromAPI(positions, isTrackDepthModified, isFromUserAction) {
return positions?.length && isTrackDepthModified && isFromUserAction
}
5 changes: 4 additions & 1 deletion frontend/src/domain/shared_slices/DisplayedError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import type { PayloadAction } from '@reduxjs/toolkit'
* A `null` value means the error is no longer displayed
*/
export type OptionalDisplayedErrorAction = {
missionFormError?: DisplayedError | null
vesselSidebarError?: DisplayedError | null
}

export type DisplayedErrorState = {
missionFormError: DisplayedError | undefined | null
vesselSidebarError: DisplayedError | undefined | null
}
const INITIAL_STATE: DisplayedErrorState = {
export const INITIAL_STATE: DisplayedErrorState = {
missionFormError: undefined,
vesselSidebarError: undefined
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/domain/use_cases/alert/validateAlert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export const validateAlert =

validateAlertFromAPI(id)
.then(() => {
// We dispatch this action to update the reportings list
// We dispatch this action to update the reporting list
// since it depends on the the alerts list that we just updated
dispatch(getVesselReportings(false))
dispatch(getVesselReportings(true))

const validatedAlert = previousAlertsWithValidatedFlag.find(alert => alert.id === id)
if (!validatedAlert) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
} from '../../shared_slices/BeaconMalfunction'
import { setDisplayedErrors } from '../../shared_slices/DisplayedError'
import { removeError } from '../../shared_slices/Global'
import { displayOrLogVesselSidebarError } from '../error/displayOrLogVesselSidebarError'
import { displayOrLogError } from '../error/displayOrLogError'

export const getVesselBeaconMalfunctions = isFromCron => async (dispatch, getState) => {
export const getVesselBeaconMalfunctions = (isFromUserAction: boolean) => async (dispatch, getState) => {
const { selectedVessel } = getState().vessel

const { loadingVesselBeaconMalfunctions, openedBeaconMalfunction, vesselBeaconMalfunctionsFromDate } =
Expand All @@ -22,7 +22,7 @@ export const getVesselBeaconMalfunctions = isFromCron => async (dispatch, getSta
return
}

if (!isFromCron) {
if (isFromUserAction) {
dispatch(loadVesselBeaconMalfunctions())
dispatch(setDisplayedErrors({ vesselSidebarError: null }))
}
Expand All @@ -40,19 +40,20 @@ export const getVesselBeaconMalfunctions = isFromCron => async (dispatch, getSta
)

if (openedBeaconMalfunction) {
dispatch(openBeaconMalfunction(openedBeaconMalfunction, isFromCron))
dispatch(openBeaconMalfunction(openedBeaconMalfunction, isFromUserAction))
}

dispatch(removeError())
} catch (error) {
dispatch(
displayOrLogVesselSidebarError(
displayOrLogError(
error as Error,
{
func: getVesselBeaconMalfunctions,
parameters: [isFromCron]
parameters: [isFromUserAction]
},
isFromCron
isFromUserAction,
'vesselSidebarError'
)
)
dispatch(resetVesselBeaconMalfunctionsResumeAndHistory())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@ import { getBeaconMalfunctionFromAPI } from '../../../api/beaconMalfunction'
* Open a single beacon malfunction
* @function openBeaconMalfunction
* @param {BeaconMalfunctionResumeAndDetails} beaconMalfunction - the beacon malfunction to open
* @param {boolean} fromCron - if the use case is called from the API Worker
* @param {boolean} isFromUserAction - if the use case is called from the API Worker
*/
const openBeaconMalfunction = (beaconMalfunction, fromCron) => (dispatch, getState) => {
const openBeaconMalfunction = (beaconMalfunction, isFromUserAction) => (dispatch, getState) => {
const previousBeaconMalfunction = getState().beaconMalfunction.openedBeaconMalfunction
dispatch(setOpenedBeaconMalfunction({
beaconMalfunction: beaconMalfunction,
showTab: !fromCron
showTab: isFromUserAction
}))

getBeaconMalfunctionFromAPI(beaconMalfunction.beaconMalfunction?.id).then(beaconMalfunctionWithDetails => {
dispatch(setOpenedBeaconMalfunction({
beaconMalfunction: beaconMalfunctionWithDetails,
showTab: !fromCron
showTab: isFromUserAction
}))
}).catch(error => {
console.error(error)
dispatch(setError(error))
dispatch(setOpenedBeaconMalfunction({
beaconMalfunction: previousBeaconMalfunction,
showTab: !fromCron
showTab: isFromUserAction
}))
})
}
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/domain/use_cases/error/displayOrLogError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DisplayedError } from '../../../libs/DisplayedError'
import { INITIAL_STATE, setDisplayedErrors } from '../../shared_slices/DisplayedError'
import { setError } from '../../shared_slices/Global'

import type { RetryableUseCase } from '../../../libs/DisplayedError'

/**
* Dispatch:
* - A toast error if the use-case was triggered by the cron
* - A displayedError to be shown in the vessel sidebar if the use-case was triggered by the user
*/
export const displayOrLogError =
(
error: Error,
retryableUseCase: RetryableUseCase | undefined,
isFromUserAction: boolean,
displayedErrorBoundary: string
) =>
async dispatch => {
/**
* If the use-case was triggered by the cron, we only log an error with a Toast
*/
if (!isFromUserAction) {
dispatch(setError(error))

return
}

/**
* Else, the use-case was an user action, we show a fallback error UI to the user.
* We first check if the `displayedErrorBoundary` is correct (included in the DisplayedErrorState type)
*/
if (!Object.keys(INITIAL_STATE).includes(displayedErrorBoundary)) {
return
}

const displayedError = new DisplayedError(error.message, retryableUseCase)
dispatch(setDisplayedErrors({ [displayedErrorBoundary]: displayedError }))
}

This file was deleted.

16 changes: 16 additions & 0 deletions frontend/src/domain/use_cases/error/retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { RetryableUseCase } from '../../../libs/DisplayedError'

export const retry = (retryableUseCase: RetryableUseCase | undefined) => async dispatch => {
if (!retryableUseCase) {
return
}

const parameters = retryableUseCase?.parameters
if (!parameters) {
dispatch(retryableUseCase?.func())

return
}

dispatch(retryableUseCase?.func(...parameters))
}
4 changes: 2 additions & 2 deletions frontend/src/domain/use_cases/map/clickOnMapFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ export const clickOnMapFeature = (mapClick: MapClick) => (dispatch, getState) =>
const clickedVessel = (mapClick.feature as VesselLastPositionFeature).vesselProperties

if (mapClick.ctrlKeyPressed) {
dispatch(showVesselTrack(clickedVessel, false, null))
dispatch(showVesselTrack(clickedVessel, true, null))
} else {
dispatch(showVessel(clickedVessel, false, false))
dispatch(showVessel(clickedVessel, false, true))
}
}
}
Loading

0 comments on commit 2ec6a29

Please sign in to comment.