Skip to content

Commit

Permalink
[Mission] Ajout de boutons pour conserver ou non une mission si l'uni…
Browse files Browse the repository at this point in the history
…té sélectionnée est déjà engagée (#2945)

## Linked issues

- Resolve #2925

----

- [x] Tests E2E (Cypress)
  • Loading branch information
louptheron authored Feb 22, 2024
2 parents 40c666b + c32f320 commit b8a490d
Show file tree
Hide file tree
Showing 15 changed files with 250 additions and 86 deletions.
4 changes: 1 addition & 3 deletions frontend/config/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { defineConfig } from 'cypress'
import initCypressMousePositionPlugin from 'cypress-mouse-position/plugin'
import { initPlugin } from 'cypress-plugin-snapshots/plugin'
import { platform } from 'os'

const IS_CI = Boolean(process.env.CI)
const IS_DARWIN = platform() === 'darwin'
const DEFAULT_PORT = IS_CI ? 8880 : 3000

export default defineConfig({
e2e: {
baseUrl: `http://${IS_DARWIN ? '0.0.0.0' : 'localhost'}:${DEFAULT_PORT}`,
baseUrl: `http://localhost:${DEFAULT_PORT}`,
excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'],
setupNodeEvents(on, config) {
initCypressMousePositionPlugin(on)
Expand Down
4 changes: 0 additions & 4 deletions frontend/config/husky/pre-commit

This file was deleted.

33 changes: 26 additions & 7 deletions frontend/cypress/e2e/side_window/mission_form/main_form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,16 +652,35 @@ context('Side Window > Mission Form > Main Form', () => {
)
})

it('Should show a warning indicating that a control unit is already engaged in a mission', () => {
it('A user can delete mission if control unit already engaged and be redirected to filtered mission list', () => {
openSideWindowNewMission()

cy.fill('Administration 1', 'Gendarmerie Maritime')
cy.fill('Unité 1', 'BSL Lorient')
cy.fill('Administration 1', 'Douane')
cy.fill('Unité 1', 'BGC Lorient - DF 36 Kan An Avel')

cy.get('body').should(
'contain',
'Cette unité est actuellement sélectionnée dans une autre mission en cours ouverte par le CNSP.'
)
// Then
cy.get('body').contains('Une autre mission, ouverte par le CNSP, est en cours avec cette unité.')
cy.clickButton("Non, l'abandonner")

cy.intercept('GET', '/bff/v1/missions*').as('getMissions')

cy.get('.TableBody').should('have.length.to.be.greaterThan', 0)
})

it('A user can create mission even if control unit already engaged', () => {
cy.intercept('POST', '/api/v1/missions').as('createMission')

openSideWindowNewMission()

cy.fill('Administration 1', 'Douane')
cy.fill('Unité 1', 'BGC Lorient - DF 36 Kan An Avel')

cy.get('body').contains('Une autre mission, ouverte par le CNSP, est en cours avec cette unité.')
cy.getDataCy('add-other-control-unit').should('be.disabled')
cy.clickButton('Oui, la conserver')
cy.getDataCy('add-other-control-unit').should('not.be.disabled')

cy.waitForLastRequest('@createMission', {}, 5).its('response.statusCode').should('eq', 200)
})

it('Should update the form When receiving a mission update', () => {
Expand Down
8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "6.0.1",
"@mtes-mct/monitor-ui": "11.6.0",
"@mtes-mct/monitor-ui": "11.8.0",
"@reduxjs/toolkit": "1.9.6",
"@sentry/browser": "7.55.2",
"@sentry/react": "7.52.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { missionFormActions } from '@features/Mission/components/MissionForm/slice'
import { missionListActions } from '@features/Mission/components/MissionList/slice'
import { monitorenvMissionApi } from '@features/Mission/monitorenvMissionApi'
import { MissionDateRangeFilter, MissionFilterType } from '@features/SideWindow/MissionList/types'
import { openSideWindowPath } from '@features/SideWindow/useCases/openSideWindowPath'
import { customDayjs, logSoftError } from '@mtes-mct/monitor-ui'
import { Mission } from 'domain/entities/mission/types'
import { SideWindowMenuKey } from 'domain/entities/sideWindow/constants'

export const cancelCreateAndRedirectToFilteredList =
({ controlUnitName, missionId }: { controlUnitName: string; missionId: number | undefined }) =>
async dispatch => {
dispatch(missionFormActions.setEngagedControlUnit(undefined))

// update filters to redirect to list with only pending mission with the control unit selected
dispatch(missionListActions.setListFilterValues({}))

const twoMonthsAgo = customDayjs().subtract(2, 'month').toISOString()
dispatch(
missionListActions.setListFilterValues({
[MissionFilterType.UNIT]: [controlUnitName],
[MissionFilterType.STATUS]: [Mission.MissionStatus.IN_PROGRESS],
[MissionFilterType.DATE_RANGE]: MissionDateRangeFilter.CUSTOM,
[MissionFilterType.CUSTOM_DATE_RANGE]: [twoMonthsAgo, customDayjs().toISOString()]
})
)

await dispatch(openSideWindowPath({ menu: SideWindowMenuKey.MISSION_LIST }, true))

if (missionId) {
try {
const response = await dispatch(monitorenvMissionApi.endpoints.deleteMission.initiate(missionId))
if ('error' in response) {
throw Error('Erreur à la suppression de la mission')
}
} catch (error) {
logSoftError({
isSideWindowError: true,
message: '`delete()` failed.',
originalError: error,
userMessage: 'Une erreur est survenue pendant la suppression de la mission.'
})
}
}
}
4 changes: 2 additions & 2 deletions frontend/src/features/MapButtons/Missions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function MissionsMenu() {
<ToggleMissionsButton
accent={Accent.TERTIARY}
data-cy="toggle-mission-layer"
Icon={isMissionsLayerDisplayed ? Icon.Hide : Icon.Display}
Icon={isMissionsLayerDisplayed ? Icon.Display : Icon.Hide}
onClick={toggleMissionsLayer}
size={Size.NORMAL}
title={isMissionsLayerDisplayed ? 'Cacher les missions' : 'Afficher les missions'}
Expand Down Expand Up @@ -84,7 +84,7 @@ export function MissionsMenu() {
style={{ color: THEME.color.gainsboro, top: 120 }}
title="Missions et contrôles"
>
<Icon.MissionAction />
<Icon.MissionAction size={26} />
</MissionMenuButton>
</Wrapper>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import { FIVE_MINUTES } from '@api/APIWorker'
import { useMainAppDispatch } from '@hooks/useMainAppDispatch'
import { useMainAppSelector } from '@hooks/useMainAppSelector'
import {
Accent,
Icon,
IconButton,
Level,
Message,
MultiSelect,
Select,
TextInput,
useNewWindow
} from '@mtes-mct/monitor-ui'
import { Accent, Icon, IconButton, MultiSelect, Select, TextInput, useNewWindow } from '@mtes-mct/monitor-ui'
import { isNotArchived } from '@utils/isNotArchived'
import { Mission } from 'domain/entities/mission/types'
import { useField } from 'formik'
import { uniqBy } from 'lodash'
import { useCallback, useMemo } from 'react'
import styled from 'styled-components'

import { ControlUnitWarningMessage } from './ControlUnitWarningMessage'
import {
findControlUnitByname,
mapControlUnitsToUniqueSortedNamesAsOptions,
mapToSortedResourcesAsOptions
} from './utils'
import { useGetEngagedControlUnitsQuery } from '../../../../monitorenvMissionApi'
import { INITIAL_MISSION_CONTROL_UNIT } from '../../constants'
import { missionFormActions } from '../../slice'

import type { MissionMainFormValues } from '../../types'
import type { Option } from '@mtes-mct/monitor-ui'
Expand All @@ -41,6 +32,7 @@ type ControlUnitSelectProps = Readonly<{
}
| undefined
index: number
missionId: number | undefined
onChange: (
index: number,
nextControlUnit: LegacyControlUnit.LegacyControlUnit | LegacyControlUnit.LegacyControlUnitDraft
Expand All @@ -52,34 +44,30 @@ export function ControlUnitSelect({
allControlUnits,
error,
index,
missionId,
onChange,
onDelete
}: ControlUnitSelectProps) {
const { newWindowContainerRef } = useNewWindow()
const selectedPath = useMainAppSelector(state => state.sideWindow.selectedPath)
const { data: engagedControlUnitsData } = useGetEngagedControlUnitsQuery(undefined, { pollingInterval: FIVE_MINUTES })
const dispatch = useMainAppDispatch()
const [{ value }, ,] = useField<LegacyControlUnit.LegacyControlUnit | LegacyControlUnit.LegacyControlUnitDraft>(
`controlUnits.${index}`
)

const engagedControlUnits = useMemo(() => {
if (!engagedControlUnitsData) {
return []
}

return engagedControlUnitsData
}, [engagedControlUnitsData])

// Include archived control units (and administrations) if they're already selected
const activeAndSelectedControlUnits = useMemo(
() => allControlUnits.filter(controlUnit => isNotArchived(controlUnit) || value.name === controlUnit.name) || [],
[allControlUnits, value]
)

const engagedControlUnit = engagedControlUnits.find(engaged => engaged.controlUnit.id === value.id)
const isLoading = !allControlUnits.length
const isEdition = selectedPath.id

const { data: engagedControlUnits = [] } = useGetEngagedControlUnitsQuery(undefined, {
skip: !!isEdition
})

const filteredNamesAsOptions = useMemo((): Option[] => {
if (!value.administration) {
return mapControlUnitsToUniqueSortedNamesAsOptions(activeAndSelectedControlUnits)
Expand Down Expand Up @@ -145,8 +133,20 @@ export function ControlUnitSelect({
}

onChange(index, nextControlUnit)

if (!isEdition) {
const controlUnitAlreadyEngaged = engagedControlUnits.find(
engaged => engaged.controlUnit.id === nextSelectedControlUnit?.id
)
if (controlUnitAlreadyEngaged) {
dispatch(missionFormActions.setEngagedControlUnit(controlUnitAlreadyEngaged))

return
}
dispatch(missionFormActions.setEngagedControlUnit(undefined))
}
},
[allControlUnits, value, index, isLoading, onChange]
[allControlUnits, dispatch, isEdition, engagedControlUnits, value, index, isLoading, onChange]
)

const handleResourcesChange = useCallback(
Expand Down Expand Up @@ -177,26 +177,6 @@ export function ControlUnitSelect({
onDelete(index)
}, [index, onDelete])

const controlUnitWarningMessage = useMemo(() => {
if (!engagedControlUnit) {
return ''
}

if (engagedControlUnit.missionSources.length === 1) {
return `Cette unité est actuellement sélectionnée dans une autre mission en cours ouverte par le ${
Mission.MissionSourceLabel[engagedControlUnit.missionSources[0]!]
}.`
}

if (engagedControlUnit.missionSources.length > 1) {
return `Cette unité est actuellement sélectionnée dans plusieurs autres missions en cours, ouvertes par le ${engagedControlUnit.missionSources
.map(source => Mission.MissionSourceLabel[source])
.join(' et le ')}.`
}

return ''
}, [engagedControlUnit])

return (
<Wrapper>
<UnitWrapper>
Expand Down Expand Up @@ -226,7 +206,7 @@ export function ControlUnitSelect({
searchable
value={value.name}
/>
{!isEdition && !!engagedControlUnit && <Message level={Level.WARNING}>{controlUnitWarningMessage}</Message>}
{!isEdition && <ControlUnitWarningMessage controlUnitIndex={index} missionId={missionId} />}
<MultiSelect
baseContainer={newWindowContainerRef.current}
disabled={isLoading || !value.administration || !value.name}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useMainAppDispatch } from '@hooks/useMainAppDispatch'
import { useMainAppSelector } from '@hooks/useMainAppSelector'
import { Accent, Button, Level, Message } from '@mtes-mct/monitor-ui'
import { Mission } from 'domain/entities/mission/types'
import { cancelCreateAndRedirectToFilteredList } from 'domain/use_cases/mission/cancelCreateAndRedirectToFilteredList'
import { useField } from 'formik'
import { useMemo } from 'react'
import styled from 'styled-components'

import { missionFormActions } from '../../slice'

export function ControlUnitWarningMessage({
controlUnitIndex,
missionId
}: {
controlUnitIndex: number
missionId: number | undefined
}) {
const dispatch = useMainAppDispatch()

const [unitField] = useField<number | undefined>(`controlUnits.${controlUnitIndex}.id`)

const engagedControlUnit = useMainAppSelector(state => state.missionForm.engagedControlUnit)

const message = useMemo(() => {
if (!engagedControlUnit) {
return ''
}

if (engagedControlUnit.missionSources.length === 1) {
const source = engagedControlUnit.missionSources[0]
if (!source) {
return ''
}

return `Une autre mission, ouverte par le ${Mission.MissionSourceLabel[source]}, est en cours avec cette unité.`
}

if (engagedControlUnit.missionSources.length > 1) {
return `D'autres missions en cours, ouvertes par le ${engagedControlUnit.missionSources
.map(source => Mission.MissionSourceLabel[source])
.join(' et le ')}, sont en cours avec cette unité.`
}

return ''
}, [engagedControlUnit])

const validate = async () => {
dispatch(missionFormActions.setEngagedControlUnit(undefined))
}

const cancel = async () => {
const controlUnitName = engagedControlUnit?.controlUnit?.name
if (controlUnitName) {
dispatch(cancelCreateAndRedirectToFilteredList({ controlUnitName, missionId }))
}
}

if (!engagedControlUnit || engagedControlUnit?.controlUnit.id !== unitField.value) {
return null
}

return (
<StyledMessage level={Level.WARNING}>
<Warning>Attention</Warning>
<div>
<span>{message}</span>
<br />
<span>Voulez-vous quand même conserver cette mission ?</span>
</div>

<ButtonsContainer>
<Button accent={Accent.WARNING} onClick={validate}>
Oui, la conserver
</Button>
<Button accent={Accent.WARNING} onClick={cancel}>
Non, l&apos;abandonner
</Button>
</ButtonsContainer>
</StyledMessage>
)
}

const StyledMessage = styled(Message)`
margin-top: 8px;
`
const Warning = styled.p`
color: ${({ theme }) => theme.color.goldenPoppy};
font-weight: bold;
`
const ButtonsContainer = styled.div`
display: flex;
justify-content: space-between;
margin-top: 16px;
`
Loading

0 comments on commit b8a490d

Please sign in to comment.