Skip to content

Commit

Permalink
Change team of context (#540)
Browse files Browse the repository at this point in the history
* wip

* setting button for changing team of a context

* settings modal

* use requestbody for patch request
  • Loading branch information
starheim98 authored Dec 12, 2024
1 parent 12f3f05 commit 913b942
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 3 deletions.
11 changes: 11 additions & 0 deletions backend/src/main/kotlin/no/bekk/database/ContextRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ object ContextRepository {
}
}
}

fun changeTeam(contextId: String, newTeamId: String): Boolean {
val sqlStatement = "UPDATE contexts SET team_id = ? WHERE id = ?"
Database.getConnection().use { conn ->
conn.prepareStatement(sqlStatement).use { statement ->
statement.setObject(1, UUID.fromString(newTeamId))
statement.setObject(2, UUID.fromString(contextId))
return statement.executeUpdate() > 0
}
}
}
}

class UniqueConstraintViolationException(message: String) : RuntimeException(message)
42 changes: 41 additions & 1 deletion backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.ktor.server.plugins.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import no.bekk.authentication.hasContextAccess
Expand Down Expand Up @@ -99,6 +100,45 @@ fun Route.contextRouting() {
ContextRepository.deleteContext(contextId)
call.respondText("Context and its answers and comments were successfully deleted.")
}

patch {
try {
logger.info("Received PATCH /contexts with id: ${call.parameters["contextId"]}")
val contextId = call.parameters["contextId"] ?: throw BadRequestException("Missing contextId")

val payload = call.receive<TeamUpdateRequest>()
val newTeamId = payload.teamId ?: throw BadRequestException("Missing teamId in request body")

if (!hasTeamAccess(call, newTeamId)) {
call.respond(HttpStatusCode.Forbidden)
return@patch
}

if (!hasContextAccess(call, contextId)) {
call.respond(HttpStatusCode.Forbidden)
return@patch
}

val success = ContextRepository.changeTeam(contextId, newTeamId)
if (success) {
call.respond(HttpStatusCode.OK)
return@patch
} else {
call.respond(HttpStatusCode.InternalServerError)
return@patch
}
} catch (e: BadRequestException) {
logger.error("Bad request: ${e.message}", e)
call.respond(HttpStatusCode.BadRequest, e.message ?: "Bad request")
} catch (e: Exception) {
logger.error("Unexpected error when processing PATCH /contexts", e)
call.respond(HttpStatusCode.InternalServerError, "An unexpected error occurred.")
}

}
}
}
}
}

@Serializable
data class TeamUpdateRequest(val teamId: String?)
8 changes: 8 additions & 0 deletions frontend/beCompliant/src/api/apiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ export const apiConfig = {
queryKey: (teamId: string) => [PATH_CONTEXTS, teamId],
url: (teamId: string) => `${API_URL_CONTEXTS}?teamId=${teamId}`,
},
forIdAndTeam: {
queryKey: (contextId: string, teamId: string) => [
PATH_CONTEXTS,
contextId,
teamId,
],
url: (contextId: string) => `${API_URL_CONTEXTS}/${contextId}`,
},
forTeamAndTable: {
queryKey: (teamId: string, tableId: string) => [
PATH_CONTEXTS,
Expand Down
125 changes: 125 additions & 0 deletions frontend/beCompliant/src/components/table/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
Button,
FormControl,
FormLabel,
HStack,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Select,
Stack,
useToast,
} from '@kvib/react';
import { useFetchUserinfo } from '../../hooks/useFetchUserinfo';
import { useFetchContext } from '../../hooks/useFetchContext';
import { useParams } from 'react-router-dom';
import { apiConfig } from '../../api/apiConfig';
import { axiosFetch } from '../../api/Fetch';

type Props = {
onOpen: () => void;
onClose: () => void;
isOpen: boolean;
};
export function SettingsModal({ onClose, isOpen }: Props) {
const params = useParams();
const contextId = params.contextId;
const toast = useToast();

const { data: userinfo } = useFetchUserinfo();
const currentContext = useFetchContext(contextId);

const handleChangeContextTeam = async (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault();
if (!contextId) return;
const form = e.target as HTMLFormElement;
const changeTeamElement = form.elements.namedItem(
'edit_team'
) as HTMLSelectElement;
if (!changeTeamElement?.value) {
return;
}
const newTeamId = changeTeamElement.value;
try {
const response = await axiosFetch({
url: apiConfig.contexts.forIdAndTeam.url(contextId),
method: 'PATCH',
data: {
teamId: newTeamId,
},
});
if (response.status === 200 || response.status === 204) {
onClose();
}
} catch (error) {
const toastId = 'change-context-team-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,
});
}
}
};

return (
<Modal onClose={onClose} isOpen={isOpen} isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>Endre skjemautfylling</ModalHeader>
<ModalBody>
<form onSubmit={handleChangeContextTeam}>
<Stack gap="1rem">
<FormControl>
<FormLabel>
Endre teamet dette skjemaet skal gjelde for:
</FormLabel>
<Select name="edit_team" placeholder="Velg team">
{userinfo?.groups.map((team) => {
if (team.id === currentContext.data?.teamId) {
return null;
}
return (
<option key={team.id} value={team.id}>
{team.displayName}
</option>
);
})}
</Select>
</FormControl>

<HStack justifyContent="end">
<Button
variant="secondary"
colorScheme="blue"
onClick={onClose}
>
Avbryt
</Button>

<Button
aria-label="Endre team"
variant="primary"
colorScheme="blue"
type="submit"
>
Lagre
</Button>
</HStack>
</Stack>
</form>
</ModalBody>
<ModalFooter></ModalFooter>
</ModalContent>
</Modal>
);
}
34 changes: 32 additions & 2 deletions frontend/beCompliant/src/pages/ActivityPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Box, Divider, Flex, Heading, Skeleton } from '@kvib/react';
import {
Box,
Divider,
Flex,
Heading,
IconButton,
Skeleton,
useDisclosure,
} from '@kvib/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { Page } from '../components/layout/Page';
import { TableComponent } from '../components/Table';
Expand All @@ -15,6 +23,7 @@ import { useFetchContext } from '../hooks/useFetchContext';
import { useFetchUserinfo } from '../hooks/useFetchUserinfo';
import { useLocalstorageState } from '../hooks/useStorageState';
import { useCallback, useEffect, useRef } from 'react';
import { SettingsModal } from '../components/table/SettingsModal';

export const ActivityPage = () => {
const params = useParams();
Expand Down Expand Up @@ -68,6 +77,12 @@ export const ActivityPage = () => {
isPending: answerIsPending,
} = useFetchAnswers(contextId);

const {
isOpen: isSettingsOpen,
onOpen: onSettingsOpen,
onClose: onSettingsClose,
} = useDisclosure();

useEffect(() => {
if (!tableData?.id) return;

Expand Down Expand Up @@ -148,7 +163,17 @@ export const ActivityPage = () => {
<Page>
<Flex flexDirection="column" marginX="10" gap="2">
<Skeleton isLoaded={!contextIsPending && !tableIsPending} fitContent>
<Heading lineHeight="1.2">{`${context?.name} - ${tableData?.name}`}</Heading>
<Flex>
<Heading lineHeight="1.2">{`${context?.name} - ${tableData?.name}`}</Heading>
<IconButton
variant="ghost"
icon="settings"
size="lg"
aria-label="Edit context"
colorScheme="blue"
onClick={() => onSettingsOpen()}
/>
</Flex>
</Skeleton>
<Skeleton
isLoaded={!tableIsPending && !answerIsPending && !commentIsPending}
Expand Down Expand Up @@ -183,6 +208,11 @@ export const ActivityPage = () => {
/>
)}
</Skeleton>
<SettingsModal
onOpen={onSettingsOpen}
onClose={onSettingsClose}
isOpen={isSettingsOpen}
/>
</Page>
);
};

0 comments on commit 913b942

Please sign in to comment.