diff --git a/backend/src/main/kotlin/no/bekk/database/ContextRepository.kt b/backend/src/main/kotlin/no/bekk/database/ContextRepository.kt index ef5bb8fbd..590de161e 100644 --- a/backend/src/main/kotlin/no/bekk/database/ContextRepository.kt +++ b/backend/src/main/kotlin/no/bekk/database/ContextRepository.kt @@ -109,6 +109,16 @@ object ContextRepository { } } } + + fun deleteContext(id: String): Boolean { + val sqlStatementContext = "DELETE FROM contexts WHERE id = ?" + Database.getConnection().use { conn -> + conn.prepareStatement(sqlStatementContext).use { statement -> + statement.setObject(1, UUID.fromString(id)) + return statement.executeUpdate() > 0 + } + } + } } class UniqueConstraintViolationException(message: String) : RuntimeException(message) diff --git a/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt b/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt index e11bc95bd..3d0107ea5 100644 --- a/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt +++ b/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt @@ -10,10 +10,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import no.bekk.authentication.hasContextAccess import no.bekk.authentication.hasTeamAccess -import no.bekk.database.AnswerRepository -import no.bekk.database.ContextRepository -import no.bekk.database.DatabaseContextRequest -import no.bekk.database.UniqueConstraintViolationException +import no.bekk.database.* import no.bekk.util.logger fun Route.contextRouting() { @@ -78,17 +75,30 @@ fun Route.contextRouting() { } - get("/{contextId}") { - logger.debug("Received GET /context with id: ${call.parameters["contextId"]}") - val contextId = call.parameters["contextId"] ?: throw BadRequestException("Missing contextId") + route("/{contextId}") { + get { + logger.debug("Received GET /context with id: ${call.parameters["contextId"]}") + val contextId = call.parameters["contextId"] ?: throw BadRequestException("Missing contextId") - if (!hasContextAccess(call, contextId)) { - call.respond(HttpStatusCode.Forbidden) + if (!hasContextAccess(call, contextId)) { + call.respond(HttpStatusCode.Forbidden) + return@get + } + val context = ContextRepository.getContext(contextId) + call.respond(HttpStatusCode.OK, Json.encodeToString(context)) return@get } - val context = ContextRepository.getContext(contextId) - call.respond(HttpStatusCode.OK, Json.encodeToString(context)) - return@get + + delete { + logger.info("Received DELETE /context with id: ${call.parameters["contextId"]}") + val contextId = call.parameters["contextId"] ?: throw BadRequestException("Missing contextId") + if (!hasContextAccess(call, contextId)) { + call.respond(HttpStatusCode.Forbidden) + return@delete + } + ContextRepository.deleteContext(contextId) + call.respondText("Context and its answers and comments were successfully deleted.") + } } } } \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V12__replace_fk_constraints_for_contexts.sql b/backend/src/main/resources/db/migration/V12__replace_fk_constraints_for_contexts.sql new file mode 100644 index 000000000..437a38fd2 --- /dev/null +++ b/backend/src/main/resources/db/migration/V12__replace_fk_constraints_for_contexts.sql @@ -0,0 +1,11 @@ +ALTER TABLE answers + DROP CONSTRAINT fk_answers_contexts, + ADD CONSTRAINT fk_answers_contexts + FOREIGN KEY (context_id) REFERENCES contexts(id) + ON DELETE CASCADE; + +ALTER TABLE comments + DROP CONSTRAINT fk_comments_contexts, + ADD CONSTRAINT fk_comments_contexts + FOREIGN KEY (context_id) REFERENCES contexts(id) + ON DELETE CASCADE; \ No newline at end of file diff --git a/frontend/beCompliant/src/components/DeleteContextModal.tsx b/frontend/beCompliant/src/components/DeleteContextModal.tsx new file mode 100644 index 000000000..e95227422 --- /dev/null +++ b/frontend/beCompliant/src/components/DeleteContextModal.tsx @@ -0,0 +1,66 @@ +import { + Button, + HStack, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + Text, +} from '@kvib/react'; +import { useDeleteContext } from '../hooks/useDeleteContext'; + +type Props = { + onOpen: () => void; + onClose: () => void; + isOpen: boolean; + contextId: string; + teamId: string; +}; +export function DeleteContextModal({ + onClose, + isOpen, + contextId, + teamId, +}: Props) { + const { mutate: deleteContext, isPending: isLoading } = useDeleteContext( + contextId, + teamId, + onClose + ); + + return ( + + + + Slett skjemautfylling + + + + Er du sikker på at du vil slette skjemautfyllingen? + + + + + + + + + + + + ); +} diff --git a/frontend/beCompliant/src/hooks/useDeleteContext.ts b/frontend/beCompliant/src/hooks/useDeleteContext.ts new file mode 100644 index 000000000..3832c97ca --- /dev/null +++ b/frontend/beCompliant/src/hooks/useDeleteContext.ts @@ -0,0 +1,43 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiConfig } from '../api/apiConfig'; +import { useToast } from '@kvib/react'; +import { axiosFetch } from '../api/Fetch'; + +export function useDeleteContext( + contextId: string, + teamId: string, + onSuccess: () => void +) { + const URL = apiConfig.contexts.byId.url(contextId); + const queryClient = useQueryClient(); + const toast = useToast(); + + return useMutation({ + mutationKey: apiConfig.contexts.byId.queryKey(contextId), + mutationFn: () => { + return axiosFetch({ + url: URL, + method: 'DELETE', + }); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: apiConfig.contexts.forTeam.queryKey(teamId), + }); + onSuccess(); + }, + onError: () => { + const toastId = 'delete-context-error'; + if (!toast.isActive(toastId)) { + toast({ + id: toastId, + title: 'Å nei!', + description: 'Det har skjedd en feil. Prøv på nytt', + status: 'error', + duration: 5000, + isClosable: true, + }); + } + }, + }); +} diff --git a/frontend/beCompliant/src/pages/FrontPage.tsx b/frontend/beCompliant/src/pages/FrontPage.tsx index 16b189e1d..463851432 100644 --- a/frontend/beCompliant/src/pages/FrontPage.tsx +++ b/frontend/beCompliant/src/pages/FrontPage.tsx @@ -7,6 +7,9 @@ import { StackDivider, VStack, Text, + Flex, + useDisclosure, + Button, } from '@kvib/react'; import { Link as ReactRouterLink } from 'react-router-dom'; import { Page } from '../components/layout/Page'; @@ -14,6 +17,7 @@ import { useFetchUserinfo } from '../hooks/useFetchUserinfo'; import { useFetchTeamContexts } from '../hooks/useFetchTeamContexts'; import { useFetchContext } from '../hooks/useFetchContext'; import { useFetchTables } from '../hooks/useFetchTables'; +import { DeleteContextModal } from '../components/DeleteContextModal'; const FrontPage = () => { const { @@ -88,6 +92,12 @@ function TeamContexts({ teamId }: { teamId: string }) { isPending: tablesIsPending, } = useFetchTables(); + const { + isOpen: isDeleteOpen, + onOpen: onDeleteOpen, + onClose: onDeleteClose, + } = useDisclosure(); + if (contextsIsPending || tablesIsPending) { return ; } @@ -112,7 +122,25 @@ function TeamContexts({ teamId }: { teamId: string }) { {table.name} {contextsForTable.map((context) => ( - + + + + + ))}