diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnit.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnit.kt index 999222043f..8a9307375a 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnit.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnit.kt @@ -10,9 +10,9 @@ class CanDeleteControlUnit( private val reportingRepository: IReportingRepository, ) { fun execute(controlUnitId: Int): Boolean { - val missions = missionRepository.findByControlUnitId(controlUnitId) - val reportings = reportingRepository.findByControlUnitId(controlUnitId) + val nonDeletedMissions = missionRepository.findByControlUnitId(controlUnitId).filter { !it.isDeleted } + val nonDeletedReportings = reportingRepository.findByControlUnitId(controlUnitId).filter { !it.isDeleted } - return missions.isEmpty() && reportings.isEmpty() + return nonDeletedMissions.isEmpty() && nonDeletedReportings.isEmpty() } } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnit.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnit.kt index 89b87d8418..3fed0d1d6b 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnit.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnit.kt @@ -3,11 +3,15 @@ package fr.gouv.cacem.monitorenv.domain.use_cases.controlUnit import fr.gouv.cacem.monitorenv.config.UseCase import fr.gouv.cacem.monitorenv.domain.exceptions.CouldNotDeleteException import fr.gouv.cacem.monitorenv.domain.repositories.IControlUnitRepository +import fr.gouv.cacem.monitorenv.domain.repositories.IMissionRepository +import fr.gouv.cacem.monitorenv.domain.repositories.IReportingRepository @UseCase class DeleteControlUnit( private val controlUnitRepository: IControlUnitRepository, private val canDeleteControlUnit: CanDeleteControlUnit, + private val missionRepository: IMissionRepository, + private val reportingRepository: IReportingRepository, ) { fun execute(controlUnitId: Int) { if (!canDeleteControlUnit.execute(controlUnitId)) { @@ -16,6 +20,29 @@ class DeleteControlUnit( ) } + val deletedMissions = missionRepository.findByControlUnitId(controlUnitId).filter { it.isDeleted } + val deletedReportings = reportingRepository.findByControlUnitId(controlUnitId).filter { it.isDeleted } + + deletedMissions.forEach { deletedMission -> + missionRepository.save( + deletedMission.copy( + controlUnits = + deletedMission.controlUnits.filter { + controlUnit -> + controlUnit.id != controlUnitId + }, + ), + ) + } + + deletedReportings.forEach { deletedReporting -> + reportingRepository.save( + deletedReporting.copy( + controlUnitId = null, + ), + ) + } + return controlUnitRepository.deleteById(controlUnitId) } } diff --git a/backend/src/main/resources/db/testdata/V666.09__insert_dummy_missions.sql b/backend/src/main/resources/db/testdata/V666.09__insert_dummy_missions.sql index d8267e9a1b..53ba362eb1 100644 --- a/backend/src/main/resources/db/testdata/V666.09__insert_dummy_missions.sql +++ b/backend/src/main/resources/db/testdata/V666.09__insert_dummy_missions.sql @@ -54,9 +54,10 @@ INSERT INTO public.missions (id, open_by, observations_cacem, facade, start_date INSERT INTO public.missions (id, open_by, observations_cacem, facade, start_datetime_utc, end_datetime_utc, geom, closed_by, deleted, observations_cnsp, mission_source, closed, mission_order, mission_types) VALUES (39, 'KEI', 'Without black box common. More reduce many trial.', 'MED', '2022-03-02 05:42:47.588693', '2022-05-03 09:16:22.588693', NULL, 'ELI', false, NULL, 'MONITORENV', true, NULL, '{LAND}'); INSERT INTO public.missions (id, open_by, observations_cacem, facade, start_datetime_utc, end_datetime_utc, geom, closed_by, deleted, observations_cnsp, mission_source, closed, mission_order, mission_types) VALUES (40, 'TAM', 'Idea tonight interesting value.', 'NAMO', '2022-03-17 13:29:55.588693', '2022-05-27 02:14:48.588693', NULL, 'RAN', false, NULL, 'MONITORENV', true, NULL, '{LAND}'); INSERT INTO public.missions (id, open_by, observations_cacem, facade, start_datetime_utc, end_datetime_utc, geom, closed_by, deleted, observations_cnsp, mission_source, closed, mission_order, mission_types) VALUES (53, 'CDA', 'Idea tonight interesting value.', 'NAMO', '2022-11-21 13:29:55.588693', '2022-11-23 02:14:48.588693', NULL, 'CDA', false, NULL, 'MONITORENV', false, NULL, '{LAND, SEA}'); +INSERT INTO public.missions (id, open_by, observations_cacem, facade, start_datetime_utc, end_datetime_utc, geom, closed_by, deleted, observations_cnsp, mission_source, closed, mission_order, mission_types) VALUES (54, 'CDA', 'A deleted mission with a control unit.', 'NAMO', '2020-01-01 00:00:00.000000', '2020-01-01 01:00:00.000000', NULL, 'ABC', true, NULL, 'MONITORENV', false, NULL, '{LAND}'); -SELECT pg_catalog.setval('public.missions_id_seq', 53, true); +SELECT pg_catalog.setval('public.missions_id_seq', 54, true); INSERT INTO missions_control_units (mission_id, control_unit_id) @@ -121,7 +122,8 @@ VALUES ( 47, 10002, 'A Team - Gimme your number'), ( 25, 10002, 'Full contact'), ( 43, 10018, 'Full contact'), - ( 53, 10018, 'Full contact'); + ( 53, 10018, 'Full contact'), + ( 54, 10029, 'Full contact'); INSERT INTO missions_control_resources (mission_id, control_resource_id) diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnitUTests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnitUTests.kt index 7b2dc1e625..ce7c8f0779 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnitUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/CanDeleteControlUnitUTests.kt @@ -13,6 +13,59 @@ import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.test.context.junit.jupiter.SpringExtension import java.time.ZonedDateTime +val FAKE_MISSION = + MissionEntity( + id = 1, + missionTypes = listOf(), + controlUnits = listOf(), + openBy = null, + closedBy = null, + observationsCacem = null, + observationsCnsp = null, + facade = null, + geom = null, + startDateTimeUtc = ZonedDateTime.now(), + endDateTimeUtc = null, + envActions = listOf(), + isClosed = false, + isDeleted = false, + isGeometryComputedFromControls = false, + missionSource = MissionSourceEnum.MONITORENV, + hasMissionOrder = false, + isUnderJdp = false, + ) + +val FAKE_REPORTING = + ReportingEntity( + id = 1, + reportingId = null, + sourceType = null, + semaphoreId = null, + controlUnitId = null, + sourceName = null, + targetType = null, + vehicleType = null, + targetDetails = null, + geom = null, + seaFront = null, + description = null, + reportType = null, + themeId = null, + subThemeIds = null, + actionTaken = null, + isControlRequired = null, + hasNoUnitAvailable = null, + createdAt = ZonedDateTime.now(), + validityTime = null, + isArchived = false, + isDeleted = false, + openBy = null, + missionId = null, + attachedToMissionAtUtc = null, + detachedFromMissionAtUtc = null, + attachedEnvActionId = null, + ) + @ExtendWith(SpringExtension::class) class CanDeleteControlUnitUTests { @MockBean @@ -22,7 +75,7 @@ class CanDeleteControlUnitUTests { private lateinit var reportingRepository: IReportingRepository @Test - fun `execute should return true when both missions and reportings are empty`() { + fun `execute should return TRUE there are neither missions nor reportings attached to this control unit`() { val controlUnitId = 1 given(missionRepository.findByControlUnitId(controlUnitId)).willReturn(listOf()) @@ -30,87 +83,62 @@ class CanDeleteControlUnitUTests { val result = CanDeleteControlUnit(missionRepository, reportingRepository).execute(controlUnitId) - assertThat(result).isTrue + assertThat(result).isTrue() + } + + @Test + fun `execute should return FALSE when there are non-deleted missions attached to this control unit`() { + val controlUnitId = 1 + + given(missionRepository.findByControlUnitId(controlUnitId)).willReturn( + listOf(FAKE_MISSION), + ) + given(reportingRepository.findByControlUnitId(controlUnitId)).willReturn(listOf()) + + val result = CanDeleteControlUnit(missionRepository, reportingRepository).execute(controlUnitId) + + assertThat(result).isFalse() } @Test - fun `execute should return false when missions are not empty`() { + fun `execute should return TRUE when there are only deleted missions attached to this control unit`() { val controlUnitId = 1 given(missionRepository.findByControlUnitId(controlUnitId)).willReturn( - listOf( - MissionEntity( - id = 1, - missionTypes = listOf(), - controlUnits = listOf(), - openBy = null, - closedBy = null, - observationsCacem = null, - observationsCnsp = null, - facade = null, - geom = null, - startDateTimeUtc = ZonedDateTime.now(), - endDateTimeUtc = null, - envActions = listOf(), - isClosed = false, - isDeleted = false, - isGeometryComputedFromControls = false, - missionSource = MissionSourceEnum.MONITORENV, - hasMissionOrder = false, - isUnderJdp = false, - - ), - ), + listOf(FAKE_MISSION.copy(isDeleted = true)), ) given(reportingRepository.findByControlUnitId(controlUnitId)).willReturn(listOf()) val result = CanDeleteControlUnit(missionRepository, reportingRepository).execute(controlUnitId) - assertThat(result).isFalse + assertThat(result).isTrue() + } + + @Test + fun `execute should return FALSE when there are non-deleted reportings attached to this control unit`() { + val controlUnitId = 1 + + given(missionRepository.findByControlUnitId(controlUnitId)).willReturn(listOf()) + given(reportingRepository.findByControlUnitId(controlUnitId)).willReturn( + listOf(FAKE_REPORTING), + ) + + val result = CanDeleteControlUnit(missionRepository, reportingRepository).execute(controlUnitId) + + assertThat(result).isFalse() } @Test - fun `execute should return false when reportings are not empty`() { + fun `execute should return TRUE when there are only deleted reportings attached to this control unit`() { val controlUnitId = 1 given(missionRepository.findByControlUnitId(controlUnitId)).willReturn(listOf()) given(reportingRepository.findByControlUnitId(controlUnitId)).willReturn( - listOf( - ReportingEntity( - id = 1, - reportingId = null, - sourceType = null, - semaphoreId = null, - controlUnitId = null, - sourceName = null, - targetType = null, - vehicleType = null, - targetDetails = null, - geom = null, - seaFront = null, - description = null, - reportType = null, - themeId = null, - subThemeIds = null, - actionTaken = null, - isControlRequired = null, - hasNoUnitAvailable = null, - createdAt = ZonedDateTime.now(), - validityTime = null, - isArchived = false, - isDeleted = false, - openBy = null, - missionId = null, - attachedToMissionAtUtc = null, - detachedFromMissionAtUtc = null, - attachedEnvActionId = null, - - ), - ), + listOf(FAKE_REPORTING.copy(isDeleted = true)), ) val result = CanDeleteControlUnit(missionRepository, reportingRepository).execute(controlUnitId) - assertThat(result).isFalse + assertThat(result).isTrue() } } diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnitUTests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnitUTests.kt index 3fa9cab6e3..c146c81930 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnitUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/controlUnit/DeleteControlUnitUTests.kt @@ -4,6 +4,8 @@ import com.nhaarman.mockitokotlin2.given import com.nhaarman.mockitokotlin2.verify import fr.gouv.cacem.monitorenv.domain.exceptions.CouldNotDeleteException import fr.gouv.cacem.monitorenv.domain.repositories.IControlUnitRepository +import fr.gouv.cacem.monitorenv.domain.repositories.IMissionRepository +import fr.gouv.cacem.monitorenv.domain.repositories.IReportingRepository import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -18,13 +20,24 @@ class DeleteControlUnitUTests { @MockBean private lateinit var canDeleteControlUnit: CanDeleteControlUnit + @MockBean + private lateinit var missionRepository: IMissionRepository + + @MockBean + private lateinit var reportingRepository: IReportingRepository + @Test fun `execute should delete control unit when canDeleteControlUnit returns true`() { val controlUnitId = 1 given(canDeleteControlUnit.execute(controlUnitId)).willReturn(true) - DeleteControlUnit(controlUnitRepository, canDeleteControlUnit).execute(controlUnitId) + DeleteControlUnit( + controlUnitRepository, + canDeleteControlUnit, + missionRepository, + reportingRepository, + ).execute(controlUnitId) verify(controlUnitRepository).deleteById(controlUnitId) } @@ -36,7 +49,12 @@ class DeleteControlUnitUTests { given(canDeleteControlUnit.execute(controlUnitId)).willReturn(false) assertThatThrownBy { - DeleteControlUnit(controlUnitRepository, canDeleteControlUnit).execute(controlUnitId) + DeleteControlUnit( + controlUnitRepository, + canDeleteControlUnit, + missionRepository, + reportingRepository, + ).execute(controlUnitId) } .isInstanceOf(CouldNotDeleteException::class.java) } diff --git a/frontend/cypress/e2e/back_office/control_unit_table/row_actions.spec.ts b/frontend/cypress/e2e/back_office/control_unit_table/row_actions.spec.ts index 5cce9d0a1d..4cd1b6e835 100644 --- a/frontend/cypress/e2e/back_office/control_unit_table/row_actions.spec.ts +++ b/frontend/cypress/e2e/back_office/control_unit_table/row_actions.spec.ts @@ -9,9 +9,16 @@ context('Back Office > Control Unit Table > Row Actions', () => { }) it('Should show an error dialog when trying to delete a control unit linked to some missions or reportings', () => { - cy.getTableRowById(10000).clickButton('Supprimer cette unité de contrôle') + cy.getTableRowByText('Cultures marines – DDTM 40').clickButton('Supprimer cette unité de contrôle') cy.get('.Component-Dialog').should('be.visible') cy.contains('Suppression impossible').should('be.visible') }) + + it("Should allow deletion of a control unit when it's only linked to a (soft) deleted mission", () => { + cy.getTableRowByText('Police Municipale Le Marin 972').clickButton('Supprimer cette unité de contrôle') + + cy.get('.Component-Dialog').should('be.visible') + cy.contains("Suppression de l'unité").should('be.visible') + }) }) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6d7665c326..6c7fafad49 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "AGPL-3.0", "dependencies": { - "@mtes-mct/monitor-ui": "11.3.0", + "@mtes-mct/monitor-ui": "11.4.0", "@reduxjs/toolkit": "1.9.7", "@rsuite/responsive-nav": "5.0.1", "@sentry/browser": "7.73.0", @@ -3891,9 +3891,9 @@ "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, "node_modules/@mtes-mct/monitor-ui": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-11.3.0.tgz", - "integrity": "sha512-b+pGRJW9tShBVy2M2QYBduDRV39l94q30T3Ck0McuPtie3b+/1j0UO6elBNgt1vtcRwkXOkUnF7pv0UpWFlPvQ==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-11.4.0.tgz", + "integrity": "sha512-IlKufMNEh/qDtGM2zBhnb00A0lL4ji81+JXUjpvo3W0oC8MJW4t99AlYCkWN4YmlmJngsv1AriLqfKizWu48Jg==", "dependencies": { "@babel/runtime": "7.22.15", "@tanstack/react-table": "8.9.7", @@ -4602,86 +4602,6 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.93.tgz", - "integrity": "sha512-gEKgk7FVIgltnIfDO6GntyuQBBlAYg5imHpRgLxB1zSI27ijVVkksc6QwISzFZAhKYaBWIsFSVeL9AYSziAF7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.93.tgz", - "integrity": "sha512-ZQPxm/fXdDQtn3yrYSL/gFfA8OfZ5jTi33yFQq6vcg/Y8talpZ+MgdSlYM0FkLrZdMTYYTNFiuBQuuvkA+av+Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.93.tgz", - "integrity": "sha512-OYFMMI2yV+aNe3wMgYhODxHdqUB/jrK0SEMHHS44GZpk8MuBXEF+Mcz4qjkY5Q1EH7KVQqXb/gVWwdgTHpjM2A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.93.tgz", - "integrity": "sha512-BT4dT78odKnJMNiq5HdjBsv29CiIdcCcImAPxeFqAeFw1LL6gh9nzI8E96oWc+0lVT5lfhoesCk4Qm7J6bty8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.93.tgz", - "integrity": "sha512-yH5fWEl1bktouC0mhh0Chuxp7HEO4uCtS/ly1Vmf18gs6wZ8DOOkgAEVv2dNKIryy+Na++ljx4Ym7C8tSJTrLw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-linux-x64-gnu": { "version": "1.3.93", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.93.tgz", @@ -4714,54 +4634,6 @@ "node": ">=10" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.93.tgz", - "integrity": "sha512-BHShlxtkven8ZjjvZ5QR6sC5fZCJ9bMujEkiha6W4cBUTY7ce7qGFyHmQd+iPC85d9kD/0cCiX/Xez8u0BhO7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.93.tgz", - "integrity": "sha512-nEwNWnz4JzYAK6asVvb92yeylfxMYih7eMQOnT7ZVlZN5ba9WF29xJ6kcQKs9HRH6MvWhz9+wRgv3FcjlU6HYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.93", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.93.tgz", - "integrity": "sha512-jibQ0zUr4kwJaQVwgmH+svS04bYTPnPw/ZkNInzxS+wFAtzINBYcU8s2PMWbDb2NGYiRSEeoSGyAvS9H+24JFA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/counter": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7890caf934..192341ade9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "test:unit:watch": "npm run test:unit -- --watch" }, "dependencies": { - "@mtes-mct/monitor-ui": "11.3.0", + "@mtes-mct/monitor-ui": "11.4.0", "@reduxjs/toolkit": "1.9.7", "@rsuite/responsive-nav": "5.0.1", "@sentry/browser": "7.73.0", diff --git a/frontend/src/api/administrationsAPI.ts b/frontend/src/api/administrationsAPI.ts index ea54f26771..8c60429ec8 100644 --- a/frontend/src/api/administrationsAPI.ts +++ b/frontend/src/api/administrationsAPI.ts @@ -37,6 +37,7 @@ export const administrationsAPI = monitorenvPublicApi.injectEndpoints({ }), canArchiveAdministration: builder.query({ + forceRefetch: () => true, query: administrationId => `/v1/administrations/${administrationId}/can_archive`, transformErrorResponse: response => new FrontendApiError(CAN_ARCHIVE_ADMINISTRATION_ERROR_MESSAGE, response), transformResponse: (response: BackendApiBooleanResponse) => response.value diff --git a/frontend/src/api/controlUnitResourcesAPI.ts b/frontend/src/api/controlUnitResourcesAPI.ts index 71436ad225..5b01a0ad0b 100644 --- a/frontend/src/api/controlUnitResourcesAPI.ts +++ b/frontend/src/api/controlUnitResourcesAPI.ts @@ -31,6 +31,7 @@ export const controlUnitResourcesAPI = monitorenvPublicApi.injectEndpoints({ }), canDeleteControlUnitResource: builder.query({ + forceRefetch: () => true, query: controlUnitResourceId => `/v1/control_unit_resources/${controlUnitResourceId}/can_delete`, transformErrorResponse: response => new FrontendApiError(CAN_DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response), diff --git a/frontend/src/api/controlUnitsAPI.ts b/frontend/src/api/controlUnitsAPI.ts index a4ad18960b..81259d9412 100644 --- a/frontend/src/api/controlUnitsAPI.ts +++ b/frontend/src/api/controlUnitsAPI.ts @@ -24,6 +24,7 @@ export const controlUnitsAPI = monitorenvPublicApi.injectEndpoints({ }), canDeleteControlUnit: builder.query({ + forceRefetch: () => true, query: controlUnitId => `/v2/control_units/${controlUnitId}/can_delete`, transformErrorResponse: response => new FrontendApiError(CAN_DELETE_CONTROL_UNIT_ERROR_MESSAGE, response), transformResponse: (response: BackendApiBooleanResponse) => response.value diff --git a/frontend/src/api/stationsAPI.ts b/frontend/src/api/stationsAPI.ts index 64aa6efbd7..be06efebfc 100644 --- a/frontend/src/api/stationsAPI.ts +++ b/frontend/src/api/stationsAPI.ts @@ -15,6 +15,7 @@ const GET_STATIONS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des export const stationsAPI = monitorenvPublicApi.injectEndpoints({ endpoints: builder => ({ canDeleteStation: builder.query({ + forceRefetch: () => true, query: stationId => `/v1/stations/${stationId}/can_delete`, transformErrorResponse: response => new FrontendApiError(CAN_DELETE_STATION_ERROR_MESSAGE, response), transformResponse: (response: BackendApiBooleanResponse) => response.value diff --git a/frontend/src/domain/shared_slices/Global.ts b/frontend/src/domain/shared_slices/Global.ts index 07f1c90e8b..9983800434 100644 --- a/frontend/src/domain/shared_slices/Global.ts +++ b/frontend/src/domain/shared_slices/Global.ts @@ -13,8 +13,7 @@ export enum ReportingContext { export enum VisibilityState { NONE = 'none', REDUCED = 'reduced', - VISIBLE = 'visible', - VISIBLE_LEFT = 'visible_left' + VISIBLE = 'visible' } export type ReportingFormVisibilityProps = { diff --git a/frontend/src/domain/use_cases/reporting/addReporting.ts b/frontend/src/domain/use_cases/reporting/addReporting.ts index e57c8cd823..ffec892081 100644 --- a/frontend/src/domain/use_cases/reporting/addReporting.ts +++ b/frontend/src/domain/use_cases/reporting/addReporting.ts @@ -1,3 +1,4 @@ +import { mainWindowActions } from '../../../features/MainWindow/slice' import { attachMissionToReportingSliceActions } from '../../../features/Reportings/slice' import { getReportingInitialValues, createIdForNewReporting } from '../../../features/Reportings/utils' import { setReportingFormVisibility, ReportingContext, VisibilityState } from '../../shared_slices/Global' @@ -18,7 +19,10 @@ export const addReporting = reporting: getReportingInitialValues({ createdAt: new Date().toISOString(), id, ...partialReporting }) } - await dispatch( + if (reportingContext === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(true)) + } + dispatch( setReportingFormVisibility({ context: reportingContext, visibility: VisibilityState.VISIBLE diff --git a/frontend/src/domain/use_cases/reporting/closeReporting.ts b/frontend/src/domain/use_cases/reporting/closeReporting.ts index d1bc88515a..76ae301035 100644 --- a/frontend/src/domain/use_cases/reporting/closeReporting.ts +++ b/frontend/src/domain/use_cases/reporting/closeReporting.ts @@ -1,3 +1,4 @@ +import { mainWindowActions } from '../../../features/MainWindow/slice' import { attachMissionToReportingSliceActions } from '../../../features/Reportings/slice' import { setReportingFormVisibility, ReportingContext, VisibilityState } from '../../shared_slices/Global' import { reportingActions } from '../../shared_slices/reporting' @@ -23,8 +24,11 @@ export const closeReporting = ) ) - await dispatch(reportingActions.setIsConfirmCancelDialogVisible(true)) - await dispatch( + dispatch(reportingActions.setIsConfirmCancelDialogVisible(true)) + if (reportingContextToClose === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(true)) + } + dispatch( setReportingFormVisibility({ context: reportingContextToClose, visibility: VisibilityState.VISIBLE @@ -39,7 +43,10 @@ export const closeReporting = } await dispatch(reportingActions.deleteSelectedReporting(reportingIdToClose)) dispatch(updateMapInteractionListeners(MapInteractionListenerEnum.NONE)) - await dispatch( + if (reportingContextToClose === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) + } + dispatch( setReportingFormVisibility({ context: reportingContextToClose, visibility: VisibilityState.NONE diff --git a/frontend/src/domain/use_cases/reporting/createMissionFromReporting.ts b/frontend/src/domain/use_cases/reporting/createMissionFromReporting.ts index 61425219f2..342b892dab 100644 --- a/frontend/src/domain/use_cases/reporting/createMissionFromReporting.ts +++ b/frontend/src/domain/use_cases/reporting/createMissionFromReporting.ts @@ -1,8 +1,9 @@ import omit from 'lodash/omit' import { reportingsAPI } from '../../../api/reportingsAPI' +import { mainWindowActions } from '../../../features/MainWindow/slice' import { isNewReporting } from '../../../features/Reportings/utils' -import { setReportingFormVisibility, setToast, VisibilityState } from '../../shared_slices/Global' +import { ReportingContext, setReportingFormVisibility, setToast, VisibilityState } from '../../shared_slices/Global' import { reportingActions } from '../../shared_slices/reporting' import { MapInteractionListenerEnum, updateMapInteractionListeners } from '../map/updateMapInteractionListeners' import { addMission } from '../missions/addMission' @@ -22,7 +23,10 @@ export const createMissionFromReporting = (values: Reporting | Partial async (dispa await dispatch(reportingActions.deleteSelectedReporting(id)) dispatch(updateMapInteractionListeners(MapInteractionListenerEnum.NONE)) + if (reportings[id].context === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) + } dispatch( setReportingFormVisibility({ context: reportings[id].context, diff --git a/frontend/src/domain/use_cases/reporting/deleteReportings.ts b/frontend/src/domain/use_cases/reporting/deleteReportings.ts index 491de89fdc..dc5453374e 100644 --- a/frontend/src/domain/use_cases/reporting/deleteReportings.ts +++ b/frontend/src/domain/use_cases/reporting/deleteReportings.ts @@ -1,6 +1,7 @@ import { reportingsAPI } from '../../../api/reportingsAPI' +import { mainWindowActions } from '../../../features/MainWindow/slice' import { attachMissionToReportingSliceActions } from '../../../features/Reportings/slice' -import { setReportingFormVisibility, setToast, VisibilityState } from '../../shared_slices/Global' +import { ReportingContext, setReportingFormVisibility, setToast, VisibilityState } from '../../shared_slices/Global' import { reportingActions } from '../../shared_slices/reporting' export const deleteReportings = (ids: number[], resetSelectionFn: () => void) => async (dispatch, getState) => { @@ -19,6 +20,9 @@ export const deleteReportings = (ids: number[], resetSelectionFn: () => void) => await dispatch(reportingActions.setActiveReportingId(undefined)) await dispatch(attachMissionToReportingSliceActions.resetAttachMissionState()) + if (context === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) + } dispatch( setReportingFormVisibility({ context, diff --git a/frontend/src/domain/use_cases/reporting/duplicateReporting.ts b/frontend/src/domain/use_cases/reporting/duplicateReporting.ts index 173f07aa03..a3eb1d9edc 100644 --- a/frontend/src/domain/use_cases/reporting/duplicateReporting.ts +++ b/frontend/src/domain/use_cases/reporting/duplicateReporting.ts @@ -34,7 +34,7 @@ export const duplicateReporting = (reportingId: number) => async (dispatch, getS ) ) - await dispatch( + dispatch( setReportingFormVisibility({ context: ReportingContext.SIDE_WINDOW, visibility: VisibilityState.VISIBLE diff --git a/frontend/src/domain/use_cases/reporting/editReportingInLocalStore.ts b/frontend/src/domain/use_cases/reporting/editReportingInLocalStore.ts index e2cf971c65..0ca9718c77 100644 --- a/frontend/src/domain/use_cases/reporting/editReportingInLocalStore.ts +++ b/frontend/src/domain/use_cases/reporting/editReportingInLocalStore.ts @@ -1,4 +1,5 @@ import { reportingsAPI } from '../../../api/reportingsAPI' +import { mainWindowActions } from '../../../features/MainWindow/slice' import { attachMissionToReportingSliceActions } from '../../../features/Reportings/slice' import { setReportingFormVisibility, setToast, ReportingContext, VisibilityState } from '../../shared_slices/Global' import { reportingActions } from '../../shared_slices/reporting' @@ -52,7 +53,8 @@ async function setReporting(dispatch, reportingId, reportingContext, newReportin ) ) - await dispatch( + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(reportingContext === ReportingContext.MAP)) + dispatch( setReportingFormVisibility({ context: reportingContext, visibility: VisibilityState.VISIBLE diff --git a/frontend/src/domain/use_cases/reporting/reduceOrCollapseReportingForm.ts b/frontend/src/domain/use_cases/reporting/reduceOrCollapseReportingForm.ts index 07d37b5dce..1332b96911 100644 --- a/frontend/src/domain/use_cases/reporting/reduceOrCollapseReportingForm.ts +++ b/frontend/src/domain/use_cases/reporting/reduceOrCollapseReportingForm.ts @@ -1,8 +1,12 @@ +import { mainWindowActions } from '../../../features/MainWindow/slice' import { setReportingFormVisibility, ReportingContext, VisibilityState } from '../../shared_slices/Global' export const reduceOrCollapseReportingForm = (reportingContext: ReportingContext) => (dispatch, getState) => { const { reportingFormVisibility } = getState().global if (reportingFormVisibility.visibility === VisibilityState.VISIBLE) { + if (reportingContext === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) + } dispatch( setReportingFormVisibility({ context: reportingContext, @@ -10,6 +14,9 @@ export const reduceOrCollapseReportingForm = (reportingContext: ReportingContext }) ) } else { + if (reportingContext === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(true)) + } dispatch( setReportingFormVisibility({ context: reportingContext, diff --git a/frontend/src/domain/use_cases/reporting/reduceReportingFormOnMap.ts b/frontend/src/domain/use_cases/reporting/reduceReportingFormOnMap.ts index 28e266f8aa..776abd232e 100644 --- a/frontend/src/domain/use_cases/reporting/reduceReportingFormOnMap.ts +++ b/frontend/src/domain/use_cases/reporting/reduceReportingFormOnMap.ts @@ -1,13 +1,20 @@ +import { mainWindowActions } from '../../../features/MainWindow/slice' import { setReportingFormVisibility, ReportingContext, VisibilityState } from '../../shared_slices/Global' -export const reduceReportingFormOnMap = () => (dispatch, getState) => { +import type { HomeAppThunk } from '../../../store' + +export const reduceReportingFormOnMap = (): HomeAppThunk => (dispatch, getState) => { const { reportingFormVisibility } = getState().global - if (reportingFormVisibility.context === ReportingContext.MAP && reportingFormVisibility !== VisibilityState.NONE) { + if ( + reportingFormVisibility.context === ReportingContext.MAP && + reportingFormVisibility.visibility !== VisibilityState.NONE + ) { dispatch( setReportingFormVisibility({ context: ReportingContext.MAP, visibility: VisibilityState.REDUCED }) ) + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) } } diff --git a/frontend/src/domain/use_cases/reporting/saveReporting.ts b/frontend/src/domain/use_cases/reporting/saveReporting.ts index b51f1310fa..6be94b36f6 100644 --- a/frontend/src/domain/use_cases/reporting/saveReporting.ts +++ b/frontend/src/domain/use_cases/reporting/saveReporting.ts @@ -2,6 +2,7 @@ import omit from 'lodash/omit' import { reportingsAPI } from '../../../api/reportingsAPI' import { ApiErrorCode } from '../../../api/types' +import { mainWindowActions } from '../../../features/MainWindow/slice' import { isNewReporting } from '../../../features/Reportings/utils' import { setReportingFormVisibility, setToast, ReportingContext, VisibilityState } from '../../shared_slices/Global' import { reportingActions } from '../../shared_slices/reporting' @@ -20,6 +21,9 @@ export const saveReporting = try { const response = await dispatch(endpoint.initiate(newOrNextReportingData)) if ('data' in response) { + if (reportingContext === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) + } dispatch( setReportingFormVisibility({ context: reportingContext, diff --git a/frontend/src/domain/use_cases/reporting/switchReporting.ts b/frontend/src/domain/use_cases/reporting/switchReporting.ts index 0b26daa0ca..6278972397 100644 --- a/frontend/src/domain/use_cases/reporting/switchReporting.ts +++ b/frontend/src/domain/use_cases/reporting/switchReporting.ts @@ -1,3 +1,4 @@ +import { mainWindowActions } from '../../../features/MainWindow/slice' import { attachMissionToReportingSliceActions } from '../../../features/Reportings/slice' import { setReportingFormVisibility, ReportingContext, VisibilityState } from '../../shared_slices/Global' import { reportingActions } from '../../shared_slices/reporting' @@ -17,7 +18,8 @@ export const switchReporting = ) ) - await dispatch( + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(reportingContext === ReportingContext.MAP)) + dispatch( setReportingFormVisibility({ context: reportingContext, visibility: VisibilityState.VISIBLE diff --git a/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/Placeholder.tsx b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/Placeholder.tsx index 3ea3e9bb5c..81a7ac0aa6 100644 --- a/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/Placeholder.tsx +++ b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/Placeholder.tsx @@ -13,13 +13,12 @@ export function Placeholder({ type }: PlaceholderProps) { } const Wrapper = styled.div` - align-items: flex-start; + align-items: center; background-color: ${p => p.theme.color.lightGray}; display: flex; flex-grow: 1; justify-content: center; max-width: 116px; min-width: 116px; - padding-top: 32px; width: 116px; ` diff --git a/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/utils.tsx b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/utils.tsx index a5a064d2f7..53c8cbec85 100644 --- a/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/utils.tsx +++ b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/utils.tsx @@ -1,10 +1,9 @@ -import { ControlUnit, Icon } from '@mtes-mct/monitor-ui' +import { ControlUnit, Icon, getControlUnitResourceCategoryFromType } from '@mtes-mct/monitor-ui' import { ControlUnit as LocalControlUnit } from '../../../../../domain/entities/controlUnit' -import { getControlUnitResourceCategoryFromControlUnitResourceType } from '../../../utils' export function getIconFromControlUnitResourceType(type: LocalControlUnit.ControlUnitResourceType) { - const category = getControlUnitResourceCategoryFromControlUnitResourceType(type) + const category = getControlUnitResourceCategoryFromType(type) switch (category) { case ControlUnit.ControlUnitResourceCategory.LAND: diff --git a/frontend/src/features/ControlUnit/components/ControlUnitDialog/index.tsx b/frontend/src/features/ControlUnit/components/ControlUnitDialog/index.tsx index 002cae64b3..3586057629 100644 --- a/frontend/src/features/ControlUnit/components/ControlUnitDialog/index.tsx +++ b/frontend/src/features/ControlUnit/components/ControlUnitDialog/index.tsx @@ -14,9 +14,11 @@ import { addMission } from '../../../../domain/use_cases/missions/addMission' import { useAppDispatch } from '../../../../hooks/useAppDispatch' import { useAppSelector } from '../../../../hooks/useAppSelector' import { FrontendError } from '../../../../libs/FrontendError' +import { mainWindowActions } from '../../../MainWindow/slice' export function ControlUnitDialog() { const dispatch = useAppDispatch() + const isRightMenuOpened = useAppSelector(store => store.mainWindow.isRightMenuOpened) const mapControlUnitDialog = useAppSelector(store => store.mapControlUnitDialog) if (!mapControlUnitDialog.controlUnitId) { throw new FrontendError('`mapControlUnitDialog.controlUnitId` is undefined.') @@ -35,6 +37,7 @@ export function ControlUnitDialog() { isControlUnitDialogVisible: false }) ) + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) }, [dispatch]) if (!controlUnit) { @@ -49,9 +52,9 @@ export function ControlUnitDialog() { } return ( - + - + {controlUnit.name} ({controlUnit.administration.name}) @@ -70,14 +73,20 @@ export function ControlUnitDialog() { ) } -const Wrapper = styled(MapMenuDialog.Container)` - bottom: 10px; +// TODO This wrapper should be a shared Env `` component to avoid logical + styling repetition. +const Wrapper = styled(MapMenuDialog.Container)<{ + $isRightMenuOpened: boolean +}>` + bottom: 0; + /* TODO Remove margin in monitor-ui. */ + margin: 0; max-height: none; position: absolute; - right: 50px; - top: 10px; - z-index: 2; + right: ${p => (p.$isRightMenuOpened ? 56 : 8)}px; + top: 0; + transition: right 0.5s ease-out, top 0.5s ease-out; width: 500px; + z-index: 2; ` const StyledMapMenuDialogBody = styled(MapMenuDialog.Body)` diff --git a/frontend/src/features/ControlUnit/components/ControlUnitListDialog/Item.tsx b/frontend/src/features/ControlUnit/components/ControlUnitListDialog/Item.tsx index f8617a7706..c0ae6f264e 100644 --- a/frontend/src/features/ControlUnit/components/ControlUnitListDialog/Item.tsx +++ b/frontend/src/features/ControlUnit/components/ControlUnitListDialog/Item.tsx @@ -11,6 +11,7 @@ import { mapActions } from '../../../../domain/shared_slices/Map' import { useAppDispatch } from '../../../../hooks/useAppDispatch' import { useAppSelector } from '../../../../hooks/useAppSelector' import { FrontendError } from '../../../../libs/FrontendError' +import { mainWindowActions } from '../../../MainWindow/slice' import { stationActions } from '../../../Station/slice' import { controlUnitDialogActions } from '../ControlUnitDialog/slice' @@ -70,6 +71,7 @@ export function Item({ controlUnit }: ItemProps) { isControlUnitListDialogVisible: false }) ) + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(true)) } return ( diff --git a/frontend/src/features/ControlUnit/components/ControlUnitListDialog/utils.ts b/frontend/src/features/ControlUnit/components/ControlUnitListDialog/utils.ts index 735d63dcea..57d8025bf7 100644 --- a/frontend/src/features/ControlUnit/components/ControlUnitListDialog/utils.ts +++ b/frontend/src/features/ControlUnit/components/ControlUnitListDialog/utils.ts @@ -1,8 +1,14 @@ -import { ControlUnit, CustomSearch, type Filter, isDefined, pluralize } from '@mtes-mct/monitor-ui' +import { + ControlUnit, + CustomSearch, + type Filter, + isDefined, + pluralize, + getControlUnitResourceCategoryFromType +} from '@mtes-mct/monitor-ui' import { isEmpty, uniq } from 'lodash/fp' import { isNotArchived } from '../../../../utils/isNotArchived' -import { getControlUnitResourceCategoryFromControlUnitResourceType } from '../../utils' import type { FiltersState } from './types' import type { Extent } from 'ol/extent' @@ -109,7 +115,7 @@ export function getFilters( const filter: Filter = controlUnits => controlUnits.reduce((previousControlUnits, controlUnit) => { const matches = controlUnit.controlUnitResources.filter(({ isArchived, type }) => { - const category = getControlUnitResourceCategoryFromControlUnitResourceType(type) + const category = getControlUnitResourceCategoryFromType(type) return !isArchived && !!category && filtersState.categories?.includes(category) }) diff --git a/frontend/src/features/ControlUnit/utils.ts b/frontend/src/features/ControlUnit/utils.ts deleted file mode 100644 index 0d0d686fe1..0000000000 --- a/frontend/src/features/ControlUnit/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ControlUnit } from '@mtes-mct/monitor-ui' - -export function getControlUnitResourceCategoryFromControlUnitResourceType( - type: ControlUnit.ControlUnitResourceType -): ControlUnit.ControlUnitResourceCategory | undefined { - switch (type) { - case ControlUnit.ControlUnitResourceType.CAR: - case ControlUnit.ControlUnitResourceType.EQUESTRIAN: - case ControlUnit.ControlUnitResourceType.MOTORCYCLE: - case ControlUnit.ControlUnitResourceType.PEDESTRIAN: - return ControlUnit.ControlUnitResourceCategory.LAND - - case ControlUnit.ControlUnitResourceType.AIRPLANE: - case ControlUnit.ControlUnitResourceType.DRONE: - case ControlUnit.ControlUnitResourceType.HELICOPTER: - return ControlUnit.ControlUnitResourceCategory.AIR - - case ControlUnit.ControlUnitResourceType.BARGE: - case ControlUnit.ControlUnitResourceType.FAST_BOAT: - case ControlUnit.ControlUnitResourceType.FRIGATE: - case ControlUnit.ControlUnitResourceType.HYDROGRAPHIC_SHIP: - case ControlUnit.ControlUnitResourceType.KAYAK: - case ControlUnit.ControlUnitResourceType.LIGHT_FAST_BOAT: - case ControlUnit.ControlUnitResourceType.MINE_DIVER: - case ControlUnit.ControlUnitResourceType.NET_LIFTER: - case ControlUnit.ControlUnitResourceType.PATROL_BOAT: - case ControlUnit.ControlUnitResourceType.PIROGUE: - case ControlUnit.ControlUnitResourceType.RIGID_HULL: - case ControlUnit.ControlUnitResourceType.SEA_SCOOTER: - case ControlUnit.ControlUnitResourceType.SEMI_RIGID: - case ControlUnit.ControlUnitResourceType.SUPPORT_SHIP: - case ControlUnit.ControlUnitResourceType.TRAINING_SHIP: - case ControlUnit.ControlUnitResourceType.TUGBOAT: - return ControlUnit.ControlUnitResourceCategory.SEA - - case ControlUnit.ControlUnitResourceType.NO_RESOURCE: - case ControlUnit.ControlUnitResourceType.OTHER: - default: - return undefined - } -} diff --git a/frontend/src/features/LocateOnMap/index.tsx b/frontend/src/features/LocateOnMap/index.tsx index 16e5a949bc..1901d92b4e 100644 --- a/frontend/src/features/LocateOnMap/index.tsx +++ b/frontend/src/features/LocateOnMap/index.tsx @@ -5,14 +5,14 @@ import styled from 'styled-components' import { getPlaceCoordinates, useGooglePlacesAPI } from '../../api/googlePlacesAPI/googlePlacesAPI' import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../domain/entities/map/constants' -import { ReportingContext, VisibilityState } from '../../domain/shared_slices/Global' import { setFitToExtent } from '../../domain/shared_slices/Map' import { useAppDispatch } from '../../hooks/useAppDispatch' import { useAppSelector } from '../../hooks/useAppSelector' export function LocateOnMap() { const dispatch = useAppDispatch() - const reportingFormVisibility = useAppSelector(state => state.global.reportingFormVisibility) + const hasFullHeightRightDialogOpen = useAppSelector(state => state.mainWindow.hasFullHeightRightDialogOpen) + const isRightMenuOpened = useAppSelector(state => state.mainWindow.isRightMenuOpened) const [searchedLocation, setSearchedLocation] = useState('') const results = useGooglePlacesAPI(searchedLocation) @@ -27,13 +27,7 @@ export function LocateOnMap() { } return ( - + ` +const Wrapper = styled.div<{ + $hasFullHeightRightDialogOpen: boolean + $isRightMenuOpened: boolean +}>` + display: flex; position: absolute; + right: ${p => + // eslint-disable-next-line no-nested-ternary + p.$hasFullHeightRightDialogOpen ? (p.$isRightMenuOpened ? 560 : 512) : 10}px; top: 10px; - right: ${p => { - switch (p.$reportingFormVisibility) { - case VisibilityState.VISIBLE: - return '512' - case VisibilityState.VISIBLE_LEFT: - return '560' - default: - return '10' - } - }}px; - width: 365px; - display: flex; transition: right 0.5s ease-out; + width: 365px; > div { flex-grow: 1; diff --git a/frontend/src/features/MainWindow/components/RightMenu/ButtonWrapper.tsx b/frontend/src/features/MainWindow/components/RightMenu/ButtonWrapper.tsx index 4aeb08fdd7..c71e44a222 100644 --- a/frontend/src/features/MainWindow/components/RightMenu/ButtonWrapper.tsx +++ b/frontend/src/features/MainWindow/components/RightMenu/ButtonWrapper.tsx @@ -1,7 +1,6 @@ import { forwardRef } from 'react' import styled from 'styled-components' -import { ReportingContext, VisibilityState } from '../../../../domain/shared_slices/Global' import { useAppSelector } from '../../../../hooks/useAppSelector' type ButtonWrapperProps = { @@ -9,16 +8,14 @@ type ButtonWrapperProps = { topPosition: number } export function ButtonWrapperWithRef({ children, topPosition }: ButtonWrapperProps, ref: React.Ref) { - const reportingFormVisibility = useAppSelector(state => state.global.reportingFormVisibility) + const hasFullHeightRightDialogOpen = useAppSelector(state => state.mainWindow.hasFullHeightRightDialogOpen) + const isRightMenuOpened = useAppSelector(state => state.mainWindow.isRightMenuOpened) return ( {children} @@ -26,12 +23,16 @@ export function ButtonWrapperWithRef({ children, topPosition }: ButtonWrapperPro ) } -const Wrapper = styled.div<{ $reportingFormVisibility: VisibilityState; $topPosition: number }>` - position: absolute; - top: ${p => p.$topPosition}px; - right: ${p => (p.$reportingFormVisibility === VisibilityState.VISIBLE ? '0' : '10')}px; +const Wrapper = styled.div<{ + $hasFullHeightRightDialogOpen: boolean + $isRightMenuOpened: boolean + $topPosition: number +}>` display: flex; justify-content: flex-end; + position: absolute; + right: ${p => (!p.$hasFullHeightRightDialogOpen || p.$isRightMenuOpened ? 10 : 0)}px; + top: ${p => p.$topPosition}px; transition: right 0.3s ease-out; ` diff --git a/frontend/src/features/MainWindow/components/RightMenu/ControlUnitListButton.tsx b/frontend/src/features/MainWindow/components/RightMenu/ControlUnitListButton.tsx index c799c8a4c0..a93500a34d 100644 --- a/frontend/src/features/MainWindow/components/RightMenu/ControlUnitListButton.tsx +++ b/frontend/src/features/MainWindow/components/RightMenu/ControlUnitListButton.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react' import { ButtonWrapper } from './ButtonWrapper' import { globalActions } from '../../../../domain/shared_slices/Global' +import { reduceReportingFormOnMap } from '../../../../domain/use_cases/reporting/reduceReportingFormOnMap' import { useAppDispatch } from '../../../../hooks/useAppDispatch' import { useAppSelector } from '../../../../hooks/useAppSelector' import { MenuWithCloseButton } from '../../../commonStyles/map/MenuWithCloseButton' @@ -14,6 +15,7 @@ export function ControlUnitListButton() { const toggleDialog = useCallback(() => { dispatch(globalActions.hideSideButtons()) + dispatch(reduceReportingFormOnMap()) dispatch(globalActions.setDisplayedItems({ isControlUnitListDialogVisible: !isControlUnitListDialogVisible })) }, [dispatch, isControlUnitListDialogVisible]) diff --git a/frontend/src/features/MainWindow/slice.ts b/frontend/src/features/MainWindow/slice.ts index 01ffdf5522..542c9f6e2c 100644 --- a/frontend/src/features/MainWindow/slice.ts +++ b/frontend/src/features/MainWindow/slice.ts @@ -1,12 +1,26 @@ -import { createSlice } from '@reduxjs/toolkit' +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' -interface MainWindowState {} -const INITIAL_STATE: MainWindowState = {} +interface MainWindowState { + hasFullHeightRightDialogOpen: boolean + isRightMenuOpened: boolean +} +const INITIAL_STATE: MainWindowState = { + hasFullHeightRightDialogOpen: false, + isRightMenuOpened: false +} const mainWindowSlice = createSlice({ initialState: INITIAL_STATE, name: 'mainWindow', - reducers: {} + reducers: { + setHasFullHeightRightDialogOpen(state, action: PayloadAction) { + state.hasFullHeightRightDialogOpen = action.payload + }, + + setIsRightMenuOpened(state, action: PayloadAction) { + state.isRightMenuOpened = action.payload + } + } }) export const mainWindowActions = mainWindowSlice.actions diff --git a/frontend/src/features/Reportings/ReportingForm/FormContent.tsx b/frontend/src/features/Reportings/ReportingForm/FormContent.tsx index 5733f095c0..4b209f8888 100644 --- a/frontend/src/features/Reportings/ReportingForm/FormContent.tsx +++ b/frontend/src/features/Reportings/ReportingForm/FormContent.tsx @@ -32,6 +32,7 @@ import { reduceOrCollapseReportingForm } from '../../../domain/use_cases/reporti import { useAppDispatch } from '../../../hooks/useAppDispatch' import { useAppSelector } from '../../../hooks/useAppSelector' import { DeleteModal } from '../../commonComponents/Modals/Delete' +import { mainWindowActions } from '../../MainWindow/slice' import { useSyncFormValuesWithRedux } from '../hooks/useSyncFormValuesWithRedux' import { attachMissionToReportingSliceActions } from '../slice' import { @@ -65,7 +66,7 @@ export function FormContent({ const isConfirmCancelDialogVisible = useAppSelector(state => state.reporting.isConfirmCancelDialogVisible) const activeReportingId = useAppSelector(state => state.reporting.activeReportingId) const reportingContext = - useAppSelector(state => (activeReportingId ? state.reporting.reportings[activeReportingId]?.context : undefined)) || + useAppSelector(state => (activeReportingId ? state.reporting.reportings[activeReportingId]?.context : undefined)) ?? ReportingContext.MAP const { dirty, errors, setFieldValue, setValues, values } = useFormikContext>() @@ -115,6 +116,9 @@ export function FormContent({ visibility: VisibilityState.NONE }) ) + if (reportingContext === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) + } } const deleteCurrentReporting = () => { @@ -132,6 +136,9 @@ export function FormContent({ visibility: VisibilityState.NONE }) ) + if (reportingContext === ReportingContext.MAP) { + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) + } } } @@ -219,7 +226,7 @@ export function FormContent({ state.mainWindow.isRightMenuOpened) const reportingFormVisibility = useAppSelector(state => state.global.reportingFormVisibility) const activeReportingId = useAppSelector(state => state.reporting.activeReportingId) const reportingContext = useAppSelector(state => @@ -70,6 +71,7 @@ export function ReportingFormWithContext({ context, totalReportings }: Reporting <> diff --git a/frontend/src/features/Reportings/index.tsx b/frontend/src/features/Reportings/index.tsx index b026bdba5c..76bf97fd11 100644 --- a/frontend/src/features/Reportings/index.tsx +++ b/frontend/src/features/Reportings/index.tsx @@ -11,6 +11,7 @@ import { useAppDispatch } from '../../hooks/useAppDispatch' import { useAppSelector } from '../../hooks/useAppSelector' export function Reportings({ context }: { context: ReportingContext }) { + const isRightMenuOpened = useAppSelector(state => state.mainWindow.isRightMenuOpened) const reportingFormVisibility = useAppSelector(state => state.global.reportingFormVisibility) const activeReportingId = useAppSelector(state => state.reporting.activeReportingId) const reportings = useAppSelector(state => state.reporting.reportings) @@ -62,6 +63,7 @@ export function Reportings({ context }: { context: ReportingContext }) { return ( ` ` const StyledContainer = styled.div<{ + $isRightMenuOpened: boolean $position: number $reportingContext: ReportingContext $reportingFormVisibility: VisibilityState @@ -107,9 +110,7 @@ const StyledContainer = styled.div<{ if (p.$reportingContext === ReportingContext.MAP) { switch (p.$reportingFormVisibility) { case VisibilityState.VISIBLE: - return 'right: 8px;' - case VisibilityState.VISIBLE_LEFT: - return 'right: 56px;' + return p.$isRightMenuOpened ? 'right: 56px;' : 'right: 8px;' case VisibilityState.REDUCED: return `right: 12px;` case VisibilityState.NONE: diff --git a/frontend/src/features/Reportings/style.ts b/frontend/src/features/Reportings/style.ts index dbb6475711..f035818950 100644 --- a/frontend/src/features/Reportings/style.ts +++ b/frontend/src/features/Reportings/style.ts @@ -199,6 +199,7 @@ export const StyledDeleteButton = styled(IconButton)` export const FormContainer = styled.div<{ $context: ReportingContext + $isRightMenuOpened: boolean $position: number $reportingFormVisibility?: VisibilityState }>` @@ -216,9 +217,7 @@ export const FormContainer = styled.div<{ if (p.$context === ReportingContext.MAP) { switch (p.$reportingFormVisibility) { case VisibilityState.VISIBLE: - return 'right: 8px;' - case VisibilityState.VISIBLE_LEFT: - return 'right: 56px;' + return p.$isRightMenuOpened ? 'right: 56px;' : 'right: 8px;' case VisibilityState.REDUCED: return `right: 12px; top: calc(100vh - ${p.$position * 52}px);` case VisibilityState.NONE: diff --git a/frontend/src/features/SideWindow/SideWindowLauncher.tsx b/frontend/src/features/SideWindow/SideWindowLauncher.tsx index 094a25c596..aee03cb89d 100644 --- a/frontend/src/features/SideWindow/SideWindowLauncher.tsx +++ b/frontend/src/features/SideWindow/SideWindowLauncher.tsx @@ -7,6 +7,7 @@ import { ReportingContext, VisibilityState, setReportingFormVisibility } from '. import { reportingActions } from '../../domain/shared_slices/reporting' import { useAppDispatch } from '../../hooks/useAppDispatch' import { useAppSelector } from '../../hooks/useAppSelector' +import { mainWindowActions } from '../MainWindow/slice' import { missionFormsActions } from '../missions/MissionForm/slice' export function SideWindowLauncher() { @@ -42,7 +43,9 @@ export function SideWindowLauncher() { const onUnload = async () => { dispatch(sideWindowActions.close()) dispatch(reportingActions.resetReportings()) + // TODO Why? dispatch(setReportingFormVisibility({ context: ReportingContext.MAP, visibility: VisibilityState.NONE })) + dispatch(mainWindowActions.setHasFullHeightRightDialogOpen(false)) dispatch(missionFormsActions.resetMissions()) } diff --git a/frontend/src/features/Station/components/StationLayer/index.ts b/frontend/src/features/Station/components/StationLayer/index.ts index 734d862231..fce6053984 100644 --- a/frontend/src/features/Station/components/StationLayer/index.ts +++ b/frontend/src/features/Station/components/StationLayer/index.ts @@ -106,14 +106,19 @@ export function StationLayer({ map, mapClickEvent }: BaseMapChildrenProps) { // --------------------------------------------------------------------------- // Layer Attachment - useEffect(() => { - map.getLayers().push(vectorLayerRef.current) + useEffect( + () => { + map.getLayers().push(vectorLayerRef.current) - return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps - map.removeLayer(vectorLayerRef.current) - } - }, [map]) + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + map.removeLayer(vectorLayerRef.current) + } + }, + + // We include `stations` to update the layers when their control unit resources change + [map, stations] + ) return null } diff --git a/frontend/src/features/Station/components/StationLayer/utils.ts b/frontend/src/features/Station/components/StationLayer/utils.ts index 932072b65f..2e98f8d455 100644 --- a/frontend/src/features/Station/components/StationLayer/utils.ts +++ b/frontend/src/features/Station/components/StationLayer/utils.ts @@ -38,9 +38,9 @@ export const getFeatureStyle = ((feature: Feature) => { fill: new Fill({ color: THEME.color.white }), - font: '12px Marianne', - offsetX: featureProps.controlUnitsCount === 1 ? 16.5 : 16, - offsetY: -35, + font: `11px 'Open Sans'`, + offsetX: 16.5, + offsetY: -35.5, text: featureProps.controlUnitsCount.toString(), textAlign: 'center' }) diff --git a/frontend/src/features/map/shared/RightMenuOnHoverArea.tsx b/frontend/src/features/map/shared/RightMenuOnHoverArea.tsx index 0c7e63c4f2..3d1b958497 100644 --- a/frontend/src/features/map/shared/RightMenuOnHoverArea.tsx +++ b/frontend/src/features/map/shared/RightMenuOnHoverArea.tsx @@ -1,54 +1,46 @@ import { useEffect, useRef } from 'react' import styled from 'styled-components' -import { setReportingFormVisibility, ReportingContext, VisibilityState } from '../../../domain/shared_slices/Global' import { useAppDispatch } from '../../../hooks/useAppDispatch' import { useAppSelector } from '../../../hooks/useAppSelector' import { useClickOutsideWhenOpened } from '../../../hooks/useClickOutsideWhenOpened' +import { mainWindowActions } from '../../MainWindow/slice' export function RightMenuOnHoverArea() { - const dispatch = useAppDispatch() - const context = useAppSelector(state => state.global.reportingFormVisibility.context) - const visibility = useAppSelector(state => state.global.reportingFormVisibility.visibility) + const areaRef = useRef(null) - const isReportingFormVisible = visibility === VisibilityState.VISIBLE || visibility === VisibilityState.VISIBLE_LEFT + const dispatch = useAppDispatch() + const hasFullHeightRightDialogOpen = useAppSelector(state => state.mainWindow.hasFullHeightRightDialogOpen) + const isRightMenuOpened = useAppSelector(state => state.mainWindow.isRightMenuOpened) - const areaRef = useRef(null) + const onMouseEnter = () => { + dispatch(mainWindowActions.setIsRightMenuOpened(true)) + } - const clickedOutsideComponent = useClickOutsideWhenOpened(areaRef, isReportingFormVisible) + const clickedOutsideComponent = useClickOutsideWhenOpened(areaRef, hasFullHeightRightDialogOpen) useEffect(() => { - if (clickedOutsideComponent && context === ReportingContext.MAP && visibility === VisibilityState.VISIBLE_LEFT) { - dispatch( - setReportingFormVisibility({ - context: ReportingContext.MAP, - visibility: VisibilityState.VISIBLE - }) - ) + if (clickedOutsideComponent) { + dispatch(mainWindowActions.setIsRightMenuOpened(false)) } // to prevent re-render // eslint-disable-next-line react-hooks/exhaustive-deps }, [clickedOutsideComponent]) - const onMouseEnter = () => { - if (context === ReportingContext.MAP && visibility === VisibilityState.VISIBLE) { - dispatch( - setReportingFormVisibility({ - context: ReportingContext.MAP, - visibility: VisibilityState.VISIBLE_LEFT - }) - ) - } - } - - return isReportingFormVisible ? : null + return hasFullHeightRightDialogOpen ? ( + + ) : ( + <> + ) } -const Area = styled.div` +const Area = styled.div<{ + $isRightMenuOpened: boolean +}>` height: 500px; - right: 0; - width: 60px; - opacity: 0; position: absolute; + right: 0; top: 56px; + width: ${p => (!p.$isRightMenuOpened ? 10 : 60)}px; + z-index: ${p => (!p.$isRightMenuOpened ? 1000 : 0)}; `