From 0e35ce42a591a74663856d0572dbe05a96df5a0b Mon Sep 17 00:00:00 2001 From: Claire Dagan Date: Thu, 16 May 2024 08:59:23 +0200 Subject: [PATCH 1/3] [Mission] save ended mission even if fields are missing --- .../missions/MissionForm/CancelEditModal.tsx | 41 +++++++++-- .../missions/MissionForm/CloseEditModal.tsx | 27 -------- .../MissionForm/GeneralInformationsForm.tsx | 1 - .../missions/MissionForm/MissionForm.tsx | 69 ++++++++----------- .../hooks/useSyncFormValuesWithRedux.ts | 10 +-- .../features/missions/MissionForm/index.tsx | 1 - .../features/missions/MissionForm/utils.ts | 11 +++ 7 files changed, 76 insertions(+), 84 deletions(-) delete mode 100644 frontend/src/features/missions/MissionForm/CloseEditModal.tsx diff --git a/frontend/src/features/missions/MissionForm/CancelEditModal.tsx b/frontend/src/features/missions/MissionForm/CancelEditModal.tsx index 486176eab0..b5c11d6fff 100644 --- a/frontend/src/features/missions/MissionForm/CancelEditModal.tsx +++ b/frontend/src/features/missions/MissionForm/CancelEditModal.tsx @@ -1,20 +1,49 @@ import { Accent, Button, Dialog, THEME } from '@mtes-mct/monitor-ui' +import { useMemo } from 'react' import styled from 'styled-components' type CancelEditModalProps = { + dirty: Boolean + isAutoSaveEnabled: Boolean + isMissionFormValid: Boolean onCancel: () => void onConfirm: () => void open: boolean } -export function CancelEditModal({ onCancel, onConfirm, open }: CancelEditModalProps) { +export function CancelEditModal({ + dirty, + isAutoSaveEnabled, + isMissionFormValid, + onCancel, + onConfirm, + open +}: CancelEditModalProps) { + const isMissionUnsaved = !isAutoSaveEnabled && dirty && isMissionFormValid + const title = isMissionUnsaved ? 'Enregistrer les modifications' : 'Enregistrement impossible' + + const body = useMemo(() => { + if (isMissionUnsaved) { + return ( + <> +

Vous êtes en train d'abandonner l'édition de la mission.

+

Voulez-vous enregistrer les modifications avant de quitter ?

+ + ) + } + + return ( + <> +

Vous êtes en train d'abandonner l'édition de la mission.

+ Si vous souhaitez enregistrer les modifications, merci de corriger les champs en erreur. + + ) + }, [isMissionUnsaved]) + return ( open && ( - Enregistrement impossible - -

Vous êtes en train d'abandonner l'édition de la mission.

- Si vous souhaitez enregistrer les modifications, merci de corriger les champs en erreur. -
+ {title} + {body} - - -
- ) - ) -} diff --git a/frontend/src/features/missions/MissionForm/GeneralInformationsForm.tsx b/frontend/src/features/missions/MissionForm/GeneralInformationsForm.tsx index ffca106ded..5163427625 100644 --- a/frontend/src/features/missions/MissionForm/GeneralInformationsForm.tsx +++ b/frontend/src/features/missions/MissionForm/GeneralInformationsForm.tsx @@ -159,7 +159,6 @@ export function GeneralInformationsForm({ name="controlUnits" /* eslint-disable-next-line react/jsx-props-no-spreading */ render={props => } - validateOnChange={false} /> diff --git a/frontend/src/features/missions/MissionForm/MissionForm.tsx b/frontend/src/features/missions/MissionForm/MissionForm.tsx index 886c8e52c7..6c13e152d0 100644 --- a/frontend/src/features/missions/MissionForm/MissionForm.tsx +++ b/frontend/src/features/missions/MissionForm/MissionForm.tsx @@ -10,7 +10,6 @@ import { } from '@mtes-mct/monitor-ui' import { useMissionEventContext } from 'context/useMissionEventContext' import { useFormikContext } from 'formik' -import { isEmpty } from 'lodash' import { useEffect, useMemo, useState } from 'react' import { generatePath } from 'react-router' import styled from 'styled-components' @@ -19,7 +18,6 @@ import { useDebouncedCallback } from 'use-debounce' import { ActionForm } from './ActionForm' import { ActionsTimeLine } from './ActionsTimeLine' import { CancelEditModal } from './CancelEditModal' -import { CloseEditModal } from './CloseEditModal' import { DeleteModal } from './DeleteModal' import { ExternalActionsModal } from './ExternalActionsModal' import { FormikSyncMissionFields } from './FormikSyncMissionFields' @@ -29,9 +27,8 @@ import { useSyncFormValuesWithRedux } from './hooks/useSyncFormValuesWithRedux' import { useUpdateOtherControlTypes } from './hooks/useUpdateOtherControlTypes' import { useUpdateSurveillance } from './hooks/useUpdateSurveillance' import { MissionFormBottomBar } from './MissionFormBottomBar' -import { NewMissionSchema } from './Schemas' import { missionFormsActions } from './slice' -import { isMissionAutoSaveEnabled, shouldSaveMission } from './utils' +import { getIsMissionFormValid, isMissionAutoSaveEnabled, shouldSaveMission } from './utils' import { missionsAPI } from '../../../api/missionsAPI' import { FrontCompletionStatus, @@ -52,11 +49,10 @@ import type { AtLeast } from '../../../types' enum ModalTypes { ACTIONS = 'ACTIONS', - CLOSE = 'CLOSE', DELETE = 'DELETE' } -type ModalProps = ModalTypes.ACTIONS | ModalTypes.DELETE | ModalTypes.CLOSE +type ModalProps = ModalTypes.ACTIONS | ModalTypes.DELETE type MissionFormProps = { engagedControlUnit: ControlUnit.EngagedControlUnit | undefined @@ -76,13 +72,7 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss const { getMissionEventById } = useMissionEventContext() const missionEvent = getMissionEventById(id) - const { - dirty, - errors: formErrors, - setFieldValue, - validateForm, - values - } = useFormikContext>() + const { dirty, setFieldValue, validateForm, values } = useFormikContext>() const previousEngagedControlUnit = usePrevious(engagedControlUnit) @@ -111,6 +101,8 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss const [openModal, setOpenModal] = useState(undefined) const [actionsSources, setActionsSources] = useState([]) + const isMissionFormValid = useMemo(() => getIsMissionFormValid(values), [values]) + // the form listens to the redux store to update the attached reportings // because of the map interaction to attach reportings useEffect(() => { @@ -162,26 +154,21 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss await dispatch(missionFormsActions.deleteSelectedMission(id)) } - const submitMission = () => { - validateForm().then(errors => { - if (isEmpty(errors)) { - dispatch(saveMission(values, false, true)) - - return - } - dispatch(sideWindowActions.setShowConfirmCancelModal(true)) - }) - } - - const confirmFormCancelation = () => { - // when auto save is disabled, and form has changes we want to display specific modal - if (!isAutoSaveEnabled && dirty && isEmpty(formErrors)) { - setOpenModal(ModalTypes.CLOSE) + const submitMission = async () => { + if (isMissionFormValid) { + // we need to validate form to reset `dirty` prop + validateForm().then(() => { + dispatch(saveMission(values, false, false)) + }) return } - if (isFormDirty) { + dispatch(sideWindowActions.setShowConfirmCancelModal(true)) + } + + const confirmFormCancelation = () => { + if ((!isAutoSaveEnabled && dirty && isMissionFormValid) || isFormDirty) { dispatch(sideWindowActions.setShowConfirmCancelModal(true)) } else { cancelForm() @@ -189,23 +176,15 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss } const validateBeforeOnChange = useDebouncedCallback(async (nextValues, forceSave) => { - if (!isAutoSaveEnabled) { + if (!isAutoSaveEnabled || engagedControlUnit || !isMissionFormValid) { return } - if (engagedControlUnit) { + if (!shouldSaveMission(selectedMission, missionEvent, nextValues) && !forceSave) { return } - try { - NewMissionSchema.validateSync(values, { abortEarly: false }) - if (!shouldSaveMission(selectedMission, missionEvent, nextValues) && !forceSave) { - return - } - - dispatch(saveMission(nextValues, false, false)) - // eslint-disable-next-line no-empty - } catch (e: any) {} + dispatch(saveMission(nextValues, false, false)) }, 300) useEffect(() => { @@ -259,8 +238,14 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss validateBeforeOnChange(nextValues, false)} /> - - + { diff --git a/frontend/src/features/missions/MissionForm/index.tsx b/frontend/src/features/missions/MissionForm/index.tsx index 65bb3ac2c7..83ce0e8e8f 100644 --- a/frontend/src/features/missions/MissionForm/index.tsx +++ b/frontend/src/features/missions/MissionForm/index.tsx @@ -64,7 +64,6 @@ export function MissionFormWrapper() { enableReinitialize initialValues={missionValues} onSubmit={noop} - validateOnBlur={false} validateOnMount validationSchema={MissionSchema} > diff --git a/frontend/src/features/missions/MissionForm/utils.ts b/frontend/src/features/missions/MissionForm/utils.ts index 4bbc0791a1..42ea7600a1 100644 --- a/frontend/src/features/missions/MissionForm/utils.ts +++ b/frontend/src/features/missions/MissionForm/utils.ts @@ -1,6 +1,7 @@ import { isEqual, omit } from 'lodash' import { MISSION_EVENT_UNSYNCHRONIZED_PROPERTIES_IN_FORM } from './constants' +import { NewMissionSchema } from './Schemas' import { isCypress } from '../../../utils/isCypress' import type { Mission, NewMission } from '../../../domain/entities/missions' @@ -62,3 +63,13 @@ export function shouldSaveMission( function filterActionsFormInternalProperties(values: Partial) { return values.envActions?.map(envAction => omit(envAction, 'durationMatchesMission')) ?? [] } + +export function getIsMissionFormValid(mission: Partial): Boolean { + try { + NewMissionSchema.validateSync(mission, { abortEarly: false }) + + return true + } catch (e: any) { + return false + } +} From 944255d1671dab2d1331cc2add8463cf8c1dc405 Mon Sep 17 00:00:00 2001 From: Claire Dagan Date: Thu, 16 May 2024 09:28:37 +0200 Subject: [PATCH 2/3] [Test] fix test e2e --- .../createMissionWithAttachedReportingAndAttachedAction.ts | 3 +-- frontend/src/features/missions/MissionForm/MissionForm.tsx | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/cypress/e2e/utils/createMissionWithAttachedReportingAndAttachedAction.ts b/frontend/cypress/e2e/utils/createMissionWithAttachedReportingAndAttachedAction.ts index 8d9a0fe364..f2817d5c6e 100644 --- a/frontend/cypress/e2e/utils/createMissionWithAttachedReportingAndAttachedAction.ts +++ b/frontend/cypress/e2e/utils/createMissionWithAttachedReportingAndAttachedAction.ts @@ -52,6 +52,7 @@ export function createMissionWithAttachedReportingAndAttachedAction() { // Attach the reporting to a mission cy.intercept('GET', '/bff/v1/missions*').as('getMissions') + cy.intercept('PUT', '/bff/v1/missions').as('createMission') cy.clickButton('missions') cy.clickButton('Ajouter une nouvelle mission') @@ -61,8 +62,6 @@ export function createMissionWithAttachedReportingAndAttachedAction() { cy.get('[name="missionTypes0"]').click({ force: true }) cy.fill('Unité 1', 'BN Toulon') - cy.intercept('PUT', '/bff/v1/missions').as('createMission') - return cy.waitForLastRequest( '@createMission', { diff --git a/frontend/src/features/missions/MissionForm/MissionForm.tsx b/frontend/src/features/missions/MissionForm/MissionForm.tsx index 6c13e152d0..43d5ffe92f 100644 --- a/frontend/src/features/missions/MissionForm/MissionForm.tsx +++ b/frontend/src/features/missions/MissionForm/MissionForm.tsx @@ -72,7 +72,7 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss const { getMissionEventById } = useMissionEventContext() const missionEvent = getMissionEventById(id) - const { dirty, setFieldValue, validateForm, values } = useFormikContext>() + const { dirty, setFieldValue, values } = useFormikContext>() const previousEngagedControlUnit = usePrevious(engagedControlUnit) @@ -156,10 +156,7 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss const submitMission = async () => { if (isMissionFormValid) { - // we need to validate form to reset `dirty` prop - validateForm().then(() => { - dispatch(saveMission(values, false, false)) - }) + dispatch(saveMission(values, false, true)) return } From cad1c28c27e3fe0a5ff4876fe8bd640c5eaa1e9d Mon Sep 17 00:00:00 2001 From: Claire Dagan Date: Thu, 16 May 2024 09:44:43 +0200 Subject: [PATCH 3/3] [Tech] changes Boolean to boolean --- .../features/missions/MissionForm/CancelEditModal.tsx | 10 +++++----- .../src/features/missions/MissionForm/MissionForm.tsx | 2 +- frontend/src/features/missions/MissionForm/utils.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/features/missions/MissionForm/CancelEditModal.tsx b/frontend/src/features/missions/MissionForm/CancelEditModal.tsx index b5c11d6fff..16a578aed6 100644 --- a/frontend/src/features/missions/MissionForm/CancelEditModal.tsx +++ b/frontend/src/features/missions/MissionForm/CancelEditModal.tsx @@ -3,22 +3,22 @@ import { useMemo } from 'react' import styled from 'styled-components' type CancelEditModalProps = { - dirty: Boolean - isAutoSaveEnabled: Boolean - isMissionFormValid: Boolean + isAutoSaveEnabled: boolean + isDirty: boolean + isMissionFormValid: boolean onCancel: () => void onConfirm: () => void open: boolean } export function CancelEditModal({ - dirty, isAutoSaveEnabled, + isDirty, isMissionFormValid, onCancel, onConfirm, open }: CancelEditModalProps) { - const isMissionUnsaved = !isAutoSaveEnabled && dirty && isMissionFormValid + const isMissionUnsaved = !isAutoSaveEnabled && isDirty && isMissionFormValid const title = isMissionUnsaved ? 'Enregistrer les modifications' : 'Enregistrement impossible' const body = useMemo(() => { diff --git a/frontend/src/features/missions/MissionForm/MissionForm.tsx b/frontend/src/features/missions/MissionForm/MissionForm.tsx index 43d5ffe92f..ca9866d63c 100644 --- a/frontend/src/features/missions/MissionForm/MissionForm.tsx +++ b/frontend/src/features/missions/MissionForm/MissionForm.tsx @@ -236,8 +236,8 @@ export function MissionForm({ engagedControlUnit, id, isNewMission, selectedMiss validateBeforeOnChange(nextValues, false)} /> omit(envAction, 'durationMatchesMission')) ?? [] } -export function getIsMissionFormValid(mission: Partial): Boolean { +export function getIsMissionFormValid(mission: Partial): boolean { try { NewMissionSchema.validateSync(mission, { abortEarly: false })