From 04f023da86d8de1dcff59276245d7d7e4426bc80 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Fri, 13 Oct 2023 15:31:09 +0200 Subject: [PATCH] wip --- .../domain/use_cases/base/DeleteBase.kt | 11 ++++ .../publicapi/ApiAdministrationsController.kt | 4 +- .../endpoints/publicapi/ApiBasesController.kt | 12 ++++ .../repositories/JpaBaseRepository.kt | 8 +++ .../publicapi/ApiBasesControllerITests.kt | 4 ++ .../repositories/JpaBaseRepositoryITests.kt | 12 ++++ frontend/src/api/basesAPI.ts | 22 ++++++- frontend/src/features/BackOffice/types.ts | 1 + .../useCases/handleModalConfirmation.ts | 5 ++ .../Base/components/BaseTable/index.tsx | 9 ++- .../Base/components/BaseTable/utils.ts | 20 ------ .../Base/components/BaseTable/utils.tsx | 62 +++++++++++++++++++ .../src/features/Base/useCases/deleteBase.ts | 43 +++++++++++++ 13 files changed, 187 insertions(+), 26 deletions(-) create mode 100644 backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/base/DeleteBase.kt delete mode 100644 frontend/src/features/Base/components/BaseTable/utils.ts create mode 100644 frontend/src/features/Base/components/BaseTable/utils.tsx create mode 100644 frontend/src/features/Base/useCases/deleteBase.ts diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/base/DeleteBase.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/base/DeleteBase.kt new file mode 100644 index 000000000..67ce944d7 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/base/DeleteBase.kt @@ -0,0 +1,11 @@ +package fr.gouv.cacem.monitorenv.domain.use_cases.base + +import fr.gouv.cacem.monitorenv.config.UseCase +import fr.gouv.cacem.monitorenv.domain.repositories.IBaseRepository + +@UseCase +class DeleteBase(private val baseRepository: IBaseRepository) { + fun execute(baseId: Int) { + baseRepository.deleteById(baseId) + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiAdministrationsController.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiAdministrationsController.kt index ead4d423c..ef2c010c3 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiAdministrationsController.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiAdministrationsController.kt @@ -48,9 +48,9 @@ class ApiAdministrationsController( fun delete( @PathParam("Administration ID") @PathVariable(name = "administrationId") - controlUnitId: Int, + administrationId: Int, ) { - deleteAdministration.execute(controlUnitId) + deleteAdministration.execute(administrationId) } @GetMapping("/{administrationId}") diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesController.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesController.kt index 375e74a83..28e22bc2a 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesController.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesController.kt @@ -1,6 +1,7 @@ package fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.publicapi import fr.gouv.cacem.monitorenv.domain.use_cases.base.CreateOrUpdateBase +import fr.gouv.cacem.monitorenv.domain.use_cases.base.DeleteBase import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBaseById import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBases import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.inputs.CreateOrUpdateBaseDataInput @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.* @Tag(name = "Bases", description = "API bases") class ApiBasesController( private val createOrUpdateBase: CreateOrUpdateBase, + private val deleteBase: DeleteBase, private val getBases: GetBases, private val getBaseById: GetBaseById, ) { @@ -33,6 +35,16 @@ class ApiBasesController( return BaseDataOutput.fromBase(createdBase) } + @DeleteMapping("/{baseId}") + @Operation(summary = "Delete a base") + fun delete( + @PathParam("Administration ID") + @PathVariable(name = "baseId") + baseId: Int, + ) { + deleteBase.execute(baseId) + } + @GetMapping("/{baseId}") @Operation(summary = "Get a base by its ID") fun get( diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepository.kt index b12798a06..937d4243e 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepository.kt @@ -15,6 +15,14 @@ class JpaBaseRepository( private val dbBaseRepository: IDBBaseRepository, ) : IBaseRepository { override fun deleteById(baseId: Int) { + val fullBase = findById(baseId) +// println(fullBase.controlUnitResources) + /*if (fullBase.controlUnitResources.isNotEmpty()) { + throw ForeignKeyConstraintException( + "Cannot delete base (ID=$baseId) due to existing relationships.", + ) + }*/ + dbBaseRepository.deleteById(baseId) } diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesControllerITests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesControllerITests.kt index 381ece240..7e2b494e1 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesControllerITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/publicapi/ApiBasesControllerITests.kt @@ -6,6 +6,7 @@ import fr.gouv.cacem.monitorenv.config.MapperConfiguration import fr.gouv.cacem.monitorenv.config.WebSecurityConfig import fr.gouv.cacem.monitorenv.domain.entities.base.BaseEntity import fr.gouv.cacem.monitorenv.domain.use_cases.base.CreateOrUpdateBase +import fr.gouv.cacem.monitorenv.domain.use_cases.base.DeleteBase import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBaseById import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBases import fr.gouv.cacem.monitorenv.domain.use_cases.base.dtos.FullBaseDTO @@ -34,6 +35,9 @@ class ApiBasesControllerITests { @MockBean private lateinit var createOrUpdateBase: CreateOrUpdateBase + @MockBean + private lateinit var deleteBase: DeleteBase + @MockBean private lateinit var getBaseById: GetBaseById diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepositoryITests.kt index 781bae27d..36c1f7e72 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaBaseRepositoryITests.kt @@ -4,6 +4,8 @@ import fr.gouv.cacem.monitorenv.domain.entities.base.BaseEntity import fr.gouv.cacem.monitorenv.domain.entities.controlUnit.ControlUnitResourceEntity import fr.gouv.cacem.monitorenv.domain.entities.controlUnit.ControlUnitResourceType import fr.gouv.cacem.monitorenv.domain.use_cases.base.dtos.FullBaseDTO +import fr.gouv.cacem.monitorenv.infrastructure.database.repositories.exceptions.ForeignKeyConstraintException +import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -13,6 +15,16 @@ class JpaBaseRepositoryITests : AbstractDBTests() { @Autowired private lateinit var jpaBaseRepository: JpaBaseRepository + @Test + @Transactional + fun `deleteById() should throw the expected exception when the base is linked to some control unit resources`() { + val throwable = Assertions.catchThrowable { + jpaBaseRepository.deleteById(1) + } + + assertThat(throwable).isInstanceOf(ForeignKeyConstraintException::class.java) + } + @Test @Transactional fun `findAll() should find all bases`() { diff --git a/frontend/src/api/basesAPI.ts b/frontend/src/api/basesAPI.ts index 01a2bdbbe..2b67a4942 100644 --- a/frontend/src/api/basesAPI.ts +++ b/frontend/src/api/basesAPI.ts @@ -1,15 +1,20 @@ import { monitorenvPublicApi } from './api' +import { DELETE_GENERIC_ERROR_MESSAGE } from './constants' +import { ApiErrorCode } from './types' import { FrontendApiError } from '../libs/FrontendApiError' +import { newUserError } from '../libs/UserError' import type { Base } from '../domain/entities/base' +const DELETE_BASE_ERROR_MESSAGE = + "Cette base est rattachée à des moyens. Veuillez l'en détacher avant de la supprimer ou bien l'archiver." const GET_BASE_ERROR_MESSAGE = "Nous n'avons pas pu récupérer cette base." const GET_BASES_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des bases." export const basesAPI = monitorenvPublicApi.injectEndpoints({ endpoints: builder => ({ createBase: builder.mutation({ - invalidatesTags: () => [{ type: 'ControlUnits' }, { type: 'Bases' }], + invalidatesTags: () => [{ type: 'Bases' }], query: newBaseData => ({ body: newBaseData, method: 'POST', @@ -17,6 +22,21 @@ export const basesAPI = monitorenvPublicApi.injectEndpoints({ }) }), + deleteBase: builder.mutation({ + invalidatesTags: () => [{ type: 'Bases' }], + query: baseId => ({ + method: 'DELETE', + url: `/v1/bases/${baseId}` + }), + transformErrorResponse: response => { + if (response.data.type === ApiErrorCode.FOREIGN_KEY_CONSTRAINT) { + return newUserError(DELETE_BASE_ERROR_MESSAGE) + } + + return new FrontendApiError(DELETE_GENERIC_ERROR_MESSAGE, response) + } + }), + getBase: builder.query({ providesTags: () => [{ type: 'Bases' }], query: baseId => `/v1/bases/${baseId}`, diff --git a/frontend/src/features/BackOffice/types.ts b/frontend/src/features/BackOffice/types.ts index 27e62bf87..2cfb089dc 100644 --- a/frontend/src/features/BackOffice/types.ts +++ b/frontend/src/features/BackOffice/types.ts @@ -16,5 +16,6 @@ export enum BackOfficeConfirmationModalActionType { 'ARCHIVE_ADMINISTRATION' = 'ARCHIVE_ADMINISTRATION', 'ARCHIVE_CONTROL_UNIT' = 'ARCHIVE_CONTROL_UNIT', 'DELETE_ADMINISTRATION' = 'DELETE_ADMINISTRATION', + 'DELETE_BASE' = 'DELETE_BASE', 'DELETE_CONTROL_UNIT' = 'DELETE_CONTROL_UNIT' } diff --git a/frontend/src/features/BackOffice/useCases/handleModalConfirmation.ts b/frontend/src/features/BackOffice/useCases/handleModalConfirmation.ts index bc5df13d3..1975a763a 100644 --- a/frontend/src/features/BackOffice/useCases/handleModalConfirmation.ts +++ b/frontend/src/features/BackOffice/useCases/handleModalConfirmation.ts @@ -1,6 +1,7 @@ import { FrontendError } from '../../../libs/FrontendError' import { archiveAdministration } from '../../Administration/useCases/archiveAdministration' import { deleteAdministration } from '../../Administration/useCases/deleteAdministration' +import { deleteBase } from '../../Base/useCases/deleteBase' import { archiveControlUnit } from '../../ControlUnit/usesCases/archiveControlUnit' import { deleteControlUnit } from '../../ControlUnit/usesCases/deleteControlUnit' import { backOfficeActions } from '../slice' @@ -27,6 +28,10 @@ export const handleModalConfirmation = (): AppThunk => async (dispatch, ge await dispatch(deleteAdministration()) break + case BackOfficeConfirmationModalActionType.DELETE_BASE: + await dispatch(deleteBase()) + break + case BackOfficeConfirmationModalActionType.DELETE_CONTROL_UNIT: await dispatch(deleteControlUnit()) break diff --git a/frontend/src/features/Base/components/BaseTable/index.tsx b/frontend/src/features/Base/components/BaseTable/index.tsx index c8420fed1..b1f93861b 100644 --- a/frontend/src/features/Base/components/BaseTable/index.tsx +++ b/frontend/src/features/Base/components/BaseTable/index.tsx @@ -2,18 +2,21 @@ import { DataTable } from '@mtes-mct/monitor-ui' import { useMemo } from 'react' import styled from 'styled-components' -import { BASE_TABLE_COLUMNS } from './constants' import { FilterBar } from './FilterBar' -import { getFilters } from './utils' +import { getBaseTableColumns, getFilters } from './utils' import { useGetBasesQuery } from '../../../../api/basesAPI' +import { useAppDispatch } from '../../../../hooks/useAppDispatch' import { useAppSelector } from '../../../../hooks/useAppSelector' import { NavButton } from '../../../../ui/NavButton' import { BACK_OFFICE_MENU_PATH, BackOfficeMenuKey } from '../../../BackOfficeMenu/constants' export function BaseTable() { const backOfficeBaseList = useAppSelector(store => store.backOfficeBaseList) + const dispatch = useAppDispatch() const { data: bases } = useGetBasesQuery() + const baseTableColumns = useMemo(() => getBaseTableColumns(dispatch), [dispatch]) + const filteredBases = useMemo(() => { if (!bases) { return undefined @@ -34,7 +37,7 @@ export function BaseTable() { Nouvelle base - + ) } diff --git a/frontend/src/features/Base/components/BaseTable/utils.ts b/frontend/src/features/Base/components/BaseTable/utils.ts deleted file mode 100644 index 43ff6533d..000000000 --- a/frontend/src/features/Base/components/BaseTable/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CustomSearch, type Filter } from '@mtes-mct/monitor-ui' - -import type { FiltersState } from './types' -import type { Base } from '../../../../domain/entities/base' - -export function getFilters(data: Base.Base[], filtersState: FiltersState): Filter[] { - const customSearch = new CustomSearch(data, ['name'], { - cacheKey: 'BACK_OFFICE_BASE_LIST', - isStrict: true - }) - const filters: Array> = [] - - if (filtersState.query && filtersState.query.trim().length > 0) { - const filter: Filter = () => customSearch.find(filtersState.query as string) - - filters.push(filter) - } - - return filters -} diff --git a/frontend/src/features/Base/components/BaseTable/utils.tsx b/frontend/src/features/Base/components/BaseTable/utils.tsx new file mode 100644 index 000000000..22020e3b4 --- /dev/null +++ b/frontend/src/features/Base/components/BaseTable/utils.tsx @@ -0,0 +1,62 @@ +import { CustomSearch, IconButton, type Filter, Size, Icon } from '@mtes-mct/monitor-ui' + +import { BASE_TABLE_COLUMNS } from './constants' +import { backOfficeActions } from '../../../BackOffice/slice' +import { BackOfficeConfirmationModalActionType } from '../../../BackOffice/types' + +import type { FiltersState } from './types' +import type { Base } from '../../../../domain/entities/base' +import type { AppDispatch } from '../../../../store' +import type { CellContext, ColumnDef } from '@tanstack/react-table' + +function deleteBase(info: CellContext, dispatch: AppDispatch) { + const base = info.getValue() + + dispatch( + backOfficeActions.openConfirmationModal({ + actionType: BackOfficeConfirmationModalActionType.DELETE_BASE, + entityId: base.id, + modalProps: { + confirmationButtonLabel: 'Supprimer', + message: `Êtes-vous sûr de vouloir supprimer la base "${base.name}" ?`, + title: `Suppression de la base` + } + }) + ) +} + +export function getBaseTableColumns(dispatch: AppDispatch): Array> { + const deleteColumn: ColumnDef = { + accessorFn: row => row, + cell: info => ( + deleteBase(info, dispatch)} + size={Size.SMALL} + title="Supprimer cette base" + /> + ), + enableSorting: false, + header: () => '', + id: 'delete', + size: 44 + } + + return [...BASE_TABLE_COLUMNS, deleteColumn] +} + +export function getFilters(data: Base.Base[], filtersState: FiltersState): Filter[] { + const customSearch = new CustomSearch(data, ['name'], { + cacheKey: 'BACK_OFFICE_BASE_LIST', + isStrict: true + }) + const filters: Array> = [] + + if (filtersState.query && filtersState.query.trim().length > 0) { + const filter: Filter = () => customSearch.find(filtersState.query as string) + + filters.push(filter) + } + + return filters +} diff --git a/frontend/src/features/Base/useCases/deleteBase.ts b/frontend/src/features/Base/useCases/deleteBase.ts new file mode 100644 index 000000000..f3d04b146 --- /dev/null +++ b/frontend/src/features/Base/useCases/deleteBase.ts @@ -0,0 +1,43 @@ +import { THEME, logSoftError } from '@mtes-mct/monitor-ui' + +import { basesAPI } from '../../../api/basesAPI' +import { FrontendError } from '../../../libs/FrontendError' +import { isUserError } from '../../../libs/UserError' +import { backOfficeActions } from '../../BackOffice/slice' + +import type { AppThunk } from '../../../store' + +export const deleteBase = (): AppThunk> => async (dispatch, getState) => { + const { confirmationModal } = getState().backOffice + if (!confirmationModal) { + throw new FrontendError('`confirmationModal` is undefined.') + } + + try { + const { error } = await dispatch(basesAPI.endpoints.deleteBase.initiate(confirmationModal.entityId) as any) + if (error) { + throw error + } + } catch (err) { + if (isUserError(err)) { + dispatch( + backOfficeActions.openDialog({ + dialogProps: { + color: THEME.color.maximumRed, + message: err.userMessage, + title: `Suppression impossible`, + titleBackgroundColor: THEME.color.maximumRed + } + }) + ) + + return + } + + logSoftError({ + message: `An error happened while deleting an base (ID=${confirmationModal.entityId}").`, + originalError: err, + userMessage: "Une erreur est survenue pendant la suppression de l'base." + }) + } +}