Skip to content

Commit

Permalink
Feature/5 add delete endpoint to delete a ticket (#30)
Browse files Browse the repository at this point in the history
Feature:
* Add new endpoint DELETE and tests
* Add Button to delete ticket and dialogue to confirm deletion of ticket

Refactoring:
* Renamed SidePanelStatus und SnackBarStatus to SidePanelConfig and SnackBarConfig
* Combined SidePanelConfig and SnackbarConfig into one Config file
* Refactoring: Removed classes to simply export functions (ApiUtils)
  • Loading branch information
jonashonecker authored Jul 1, 2024
1 parent b7b127d commit 3cd1320
Show file tree
Hide file tree
Showing 21 changed files with 325 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public Ticket createTicket(@Valid @RequestBody NewTicketDTO newTicketDTO) {
public Ticket updateTicket(@Valid @RequestBody UpdateTicketDTO updateTicketDTO) {
return ticketService.updateTicket(updateTicketDTO);
}

@DeleteMapping("/{id}")
public void deleteTicket(@PathVariable String id) {
ticketService.deleteTicket(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,9 @@ public Ticket updateTicket(UpdateTicketDTO updateTicketDTO) {
existingTicket.author()
));
}

public void deleteTicket(String id) {
Ticket ticket = getTicketById(id);
ticketRepository.delete(ticket);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.anyOf;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
Expand Down Expand Up @@ -325,4 +328,23 @@ void updateTicket_whenInvalidIdAndTitleAndDescription_thenReturnApiErrorMessage(
.andExpect(jsonPath("$.error", containsString("title (Title must not be empty)")));
}

@Test
@WithMockUser
@DirtiesContext
void deleteTicket_whenDeletingTicketFromRepository_thenOnlyTheSpecifiedTicketIsDeleted() throws Exception {
//GIVEN
TicketScoutUser ticketScoutUser = new TicketScoutUser("test-name", "test-avatarUrl");
Ticket ticketToDelete = new Ticket("1", "projectName", "titleToDelete", "description", Status.OPEN, ticketScoutUser);
Ticket ticketToKeep = new Ticket("2", "projectName", "titleToKeep", "description", Status.OPEN, ticketScoutUser);
ticketRepository.insert(ticketToDelete);
ticketRepository.insert(ticketToKeep);

//WHEN
mockMvc.perform(delete("/api/ticket/{id}", ticketToDelete.id()))
//THEN
.andExpect(status().isOk());

assertFalse(ticketRepository.findById(ticketToDelete.id()).isPresent());
assertTrue(ticketRepository.findById(ticketToKeep.id()).isPresent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,39 @@ void updateTicket_whenUpdateTicketTitle_returnTicketWithUpdatedTitle() {
verify(ticketRepository, times(1)).findById(id);
assertEquals(expected, actual);
}

@Test
void deleteTicket_whenIdDoesNotExist_throwNoSuchTicketException() {
//GIVEN
String id = "test-id";
when(ticketRepository.findById(id)).thenReturn(Optional.empty());

//WHEN
NoSuchTicketException actual = assertThrows(NoSuchTicketException.class, () -> ticketService.deleteTicket(id));

//THEN
assertEquals("Could not find ticket with id: " + id, actual.getMessage());
}

@Test
void deleteTicket_whenIdExist_thenGetTicketByIdAndDeleteTicketByIdIsCalled() {
//GIVEN
String id = "test-id";
Ticket ticket = new Ticket(
id,
"test-projectName",
"test-title",
"test-description",
Status.OPEN,
new TicketScoutUser("test-name", "test-avatarUrl")
);
when(ticketRepository.findById(id)).thenReturn(Optional.of(ticket));

//WHEN
ticketService.deleteTicket(id);

//THEN
verify(ticketRepository, times(1)).findById(id);
verify(ticketRepository, times(1)).delete(ticket);
}
}
4 changes: 2 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ProtectedRoute from "./components/utils/ProtectedRoute.tsx";
import { User } from "./types/User.ts";
import Theme from "./components/theme/Theme.tsx";
import { Ticket } from "./types/Ticket.ts";
import ApiUtils from "./components/utils/ApiRequests.tsx";
import { getUser } from "./components/utils/ApiRequests.tsx";

export default function App() {
const [user, setUser] = useState<User | null | undefined>(undefined);
Expand All @@ -15,7 +15,7 @@ export default function App() {
);

const loadUser = () => {
ApiUtils.getUser()
getUser()
.then((response) => {
setUser(response.data);
})
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/buttons/DeleteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Button } from "@mui/material";

type DeleteButtonProps = {
onClick: () => void;
};

export default function DeleteButton({ onClick }: Readonly<DeleteButtonProps>) {
return (
<Button
onClick={onClick}
color={"error"}
variant="contained"
size={"small"}
sx={{
fontWeight: "bold",
}}
>
Delete
</Button>
);
}
8 changes: 4 additions & 4 deletions frontend/src/components/buttons/MainMenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { Dispatch, MouseEvent, SetStateAction, useState } from "react";
import { useNavigate } from "react-router-dom";
import SearchIcon from "@mui/icons-material/Search";
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
import { SidepanelStatus } from "../../types/SidepanelStatus.ts";
import { SidepanelConfig } from "../../types/Config.ts";

type MainMenuButtonProps = {
setSidepanelStatus: Dispatch<SetStateAction<SidepanelStatus>>;
setSidepanelConfig: Dispatch<SetStateAction<SidepanelConfig>>;
};

export default function MainMenuButton({
setSidepanelStatus,
setSidepanelConfig,
}: Readonly<MainMenuButtonProps>) {
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
Expand All @@ -31,7 +31,7 @@ export default function MainMenuButton({
};

function openNewTicketForm() {
setSidepanelStatus({
setSidepanelConfig({
open: true,
formType: "NewTicket",
});
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/card/TicketCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ import {
import TicketStatusChip from "../chip/TicketStatusChip.tsx";
import { Ticket } from "../../types/Ticket.ts";
import { Dispatch, SetStateAction } from "react";
import { SidepanelStatus } from "../../types/SidepanelStatus.ts";
import { SidepanelConfig } from "../../types/Config.ts";

type TicketCardProps = {
ticket: Ticket;
setSidepanelStatus: Dispatch<SetStateAction<SidepanelStatus>>;
setSidepanelConfig: Dispatch<SetStateAction<SidepanelConfig>>;
};

export default function TicketCard({
ticket,
setSidepanelStatus,
setSidepanelConfig,
}: Readonly<TicketCardProps>) {
return (
<Card variant={"elevation"} sx={{ height: "100%" }}>
<CardActionArea
onClick={() =>
setSidepanelStatus({
setSidepanelConfig({
open: true,
formType: "UpdateTicket",
ticket: ticket,
Expand Down
86 changes: 86 additions & 0 deletions frontend/src/components/dialogues/ConfirmDeletionDialogue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";
import CancelButton from "../buttons/CancelButton.tsx";
import { Dispatch, SetStateAction } from "react";
import DeleteButton from "../buttons/DeleteButton.tsx";
import { SnackbarConfig, SidepanelConfig } from "../../types/Config.ts";
import { deleteTicket } from "../utils/ApiRequests.tsx";
import { Ticket } from "../../types/Ticket.ts";

type ConfirmDeletionDialogueProps = {
confirmDeletion: boolean;
setConfirmDeletion: Dispatch<SetStateAction<boolean>>;
sidePanelConfig: SidepanelConfig;
setSidepanelConfig: Dispatch<SetStateAction<SidepanelConfig>>;
searchResults: Ticket[] | undefined;
setSearchResults: Dispatch<SetStateAction<Ticket[] | undefined>>;
setSnackbarConfig: Dispatch<SetStateAction<SnackbarConfig>>;
};

export default function ConfirmDeletionDialogue({
confirmDeletion,
setConfirmDeletion,
sidePanelConfig,
setSidepanelConfig,
searchResults,
setSearchResults,
setSnackbarConfig,
}: Readonly<ConfirmDeletionDialogueProps>) {
if (sidePanelConfig.formType !== "UpdateTicket") {
return;
}

const handleDelete = () => {
deleteTicket(sidePanelConfig.ticket.id)
.then(() => {
setSearchResults(
searchResults?.filter((ticket) => {
return ticket.id !== sidePanelConfig.ticket.id;
}),
);
setSnackbarConfig({
open: true,
severity: "success",
message: "Ticket successfully deleted!",
});
setConfirmDeletion(false);
setSidepanelConfig({ ...sidePanelConfig, open: false });
})
.catch((error) => {
setSnackbarConfig({
open: true,
severity: "error",
message: error.response.data.error,
});
});
};

const handleClose = () => {
setConfirmDeletion(false);
};

return (
<Dialog
open={confirmDeletion}
keepMounted
onClose={handleClose}
aria-describedby="alert-dialog-slide-description"
>
<DialogTitle>{"Confirm Deletion"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-slide-description">
Do you really want to delete this ticket?
</DialogContentText>
</DialogContent>
<DialogActions>
<CancelButton onClick={handleClose} />
<DeleteButton onClick={handleDelete} />
</DialogActions>
</Dialog>
);
}
6 changes: 2 additions & 4 deletions frontend/src/components/forms/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Divider, IconButton, InputBase, Paper } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import { Ticket } from "../../types/Ticket.ts";
import { Dispatch, SetStateAction, FormEvent } from "react";
import ApiUtils from "../utils/ApiRequests.tsx";
import { getAllTickets } from "../utils/ApiRequests.tsx";

type SearchFormProps = {
setSearchResults: Dispatch<SetStateAction<Ticket[] | undefined>>;
Expand All @@ -13,9 +13,7 @@ export default function SearchForm({
}: Readonly<SearchFormProps>) {
function searchTickets(event: FormEvent) {
event.preventDefault();
ApiUtils.getAllTickets().then((response) =>
setSearchResults(response.data),
);
getAllTickets().then((response) => setSearchResults(response.data));
}

return (
Expand Down
Loading

0 comments on commit 3cd1320

Please sign in to comment.