diff --git a/frontend/cypress/integration/stubbed/board.spec.js b/frontend/cypress/integration/stubbed/board.spec.js index c0999225..a30496d3 100644 --- a/frontend/cypress/integration/stubbed/board.spec.js +++ b/frontend/cypress/integration/stubbed/board.spec.js @@ -56,6 +56,9 @@ context("Board Detail (Member)", () => { cy.findAllByTestId("board-name-textarea").should("not.exist"); }); }); + it("should not see delete board button", () => { + cy.findAllByTestId("delete-board").should("not.exist"); + }); }); context("Board Detail (Owner)", () => { @@ -232,4 +235,22 @@ context("Board Detail (Owner)", () => { }); }); }); + it("should delete board", () => { + const stub = cy.stub(); + stub.onFirstCall().returns(true); + cy.on("window:confirm", stub); + cy.fixture("internals_board").then((board) => { + cy.route("DELETE", `/api/boards/${board.id}/`, ""); + + cy.findByText(board.name).should("be.visible"); + + cy.findByTestId(`board`).within(() => { + cy.findByTestId("delete-board").click(); + }); + + cy.findAllByText(board.name).should("not.exist"); + cy.findByText("All Boards").should("be.visible"); + cy.findByText(/Create new board/i).should("be.visible"); + }); + }); }); diff --git a/frontend/src/features/board/BoardBar.tsx b/frontend/src/features/board/BoardBar.tsx index 93400553..af272c48 100644 --- a/frontend/src/features/board/BoardBar.tsx +++ b/frontend/src/features/board/BoardBar.tsx @@ -6,6 +6,7 @@ import { barHeight } from "const"; import { AvatarGroup } from "@material-ui/lab"; import { css } from "@emotion/core"; import { avatarStyles } from "styles"; +import { useHistory } from "react-router-dom"; import BoardName from "components/BoardName"; import MemberInvite from "features/member/MemberInvite"; import MemberDetail from "features/member/MemberDetail"; @@ -17,10 +18,11 @@ import { Button } from "@material-ui/core"; import { PRIMARY } from "utils/colors"; import { addColumn } from "features/column/ColumnSlice"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus, faPen } from "@fortawesome/free-solid-svg-icons"; +import { faPlus, faPen, faTrash } from "@fortawesome/free-solid-svg-icons"; import { setDialogOpen } from "features/label/LabelSlice"; import LabelDialog from "features/label/LabelDialog"; import { useParams } from "react-router-dom"; +import { deleteBoard } from "features/board/BoardSlice"; import { selectAllMembers, setMemberListOpen, @@ -64,11 +66,12 @@ const buttonStyles = css` const BoardBar = () => { const dispatch = useDispatch(); + const history = useHistory(); const members = useSelector(selectAllMembers); const error = useSelector((state: RootState) => state.board.detailError); const detail = useSelector((state: RootState) => state.board.detail); const boardOwner = useSelector(currentBoardOwner); - const { id } = useParams(); + const { id } = useParams<{ id: string }>(); const detailDataExists = detail?.id.toString() === id; if (!detailDataExists || error || !detail) { @@ -83,6 +86,17 @@ const BoardBar = () => { dispatch(setDialogOpen(true)); }; + const handleDelete = () => { + if ( + window.confirm( + "Are you sure? Deleting the board will also delete related columns and tasks, and this cannot be undone." + ) + ) { + dispatch(deleteBoard(id)); + history.push("/boards"); + } + }; + return ( @@ -149,6 +163,20 @@ const BoardBar = () => { > Add Column + {boardOwner && ( + + )} diff --git a/frontend/src/features/board/BoardSlice.tsx b/frontend/src/features/board/BoardSlice.tsx index f347e9ab..dffd4484 100644 --- a/frontend/src/features/board/BoardSlice.tsx +++ b/frontend/src/features/board/BoardSlice.tsx @@ -1,6 +1,7 @@ import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit"; import { Board, IColumn, Id, ITask, Label, NanoBoard } from "types"; import api, { API_BOARDS } from "api"; +import { createInfoToast } from "features/toast/ToastSlice"; import { RootState } from "store"; import { logout } from "features/auth/AuthSlice"; @@ -40,6 +41,15 @@ export const patchBoard = createAsyncThunk< return response.data; }); +export const deleteBoard = createAsyncThunk( + "board/deleteBoardStatus", + async (id, { dispatch }) => { + await api.delete(`${API_BOARDS}${id}/`); + dispatch(createInfoToast("Board deleted")); + return id; + } +); + interface ColumnsResponse extends IColumn { tasks: ITask[]; } @@ -146,6 +156,13 @@ export const slice = createSlice({ state.detailError = undefined; state.detailLoading = false; }); + builder.addCase(deleteBoard.fulfilled, (state, action) => { + const indexOfDeletedBoard = state.all.findIndex( + (board) => board.id.toString() === action.payload + ); + state.all.splice(indexOfDeletedBoard, 1); + state.detail = null; + }); builder.addCase(logout.fulfilled, (state) => { state.all = []; state.detail = null; diff --git a/frontend/src/features/column/ColumnSlice.tsx b/frontend/src/features/column/ColumnSlice.tsx index c0152122..4dc74e99 100644 --- a/frontend/src/features/column/ColumnSlice.tsx +++ b/frontend/src/features/column/ColumnSlice.tsx @@ -3,7 +3,7 @@ import { createEntityAdapter, createAsyncThunk, } from "@reduxjs/toolkit"; -import { fetchBoardById } from "features/board/BoardSlice"; +import { deleteBoard, fetchBoardById } from "features/board/BoardSlice"; import { IColumn, Id } from "types"; import api, { API_SORT_COLUMNS, API_COLUMNS } from "api"; import { createErrorToast, createInfoToast } from "features/toast/ToastSlice"; @@ -75,6 +75,9 @@ export const slice = createSlice({ builder.addCase(deleteColumn.fulfilled, (state, action) => { columnAdapter.removeOne(state, action.payload); }); + builder.addCase(deleteBoard.fulfilled, (state) => { + columnAdapter.removeAll(state); + }); }, }); diff --git a/frontend/src/features/comment/CommentSlice.tsx b/frontend/src/features/comment/CommentSlice.tsx index eb3256d0..6591341b 100644 --- a/frontend/src/features/comment/CommentSlice.tsx +++ b/frontend/src/features/comment/CommentSlice.tsx @@ -10,6 +10,7 @@ import { createInfoToast, createSuccessToast, } from "features/toast/ToastSlice"; +import { deleteBoard } from "features/board/BoardSlice"; import { RootState } from "store"; import { Id, NewTaskComment, Status, TaskComment } from "types"; @@ -93,6 +94,9 @@ export const slice = createSlice({ builder.addCase(deleteComment.fulfilled, (state, action) => { commentAdapter.removeOne(state, action.payload as Id); }); + builder.addCase(deleteBoard.fulfilled, (state) => { + commentAdapter.removeAll(state); + }); }, }); diff --git a/frontend/src/features/label/LabelSlice.tsx b/frontend/src/features/label/LabelSlice.tsx index 418ec844..0aaee109 100644 --- a/frontend/src/features/label/LabelSlice.tsx +++ b/frontend/src/features/label/LabelSlice.tsx @@ -5,7 +5,7 @@ import { PayloadAction, } from "@reduxjs/toolkit"; import { Label, Id } from "types"; -import { fetchBoardById } from "features/board/BoardSlice"; +import { deleteBoard, fetchBoardById } from "features/board/BoardSlice"; import { RootState } from "store"; import api, { API_LABELS } from "api"; import { createInfoToast, createErrorToast } from "features/toast/ToastSlice"; @@ -84,6 +84,9 @@ export const slice = createSlice({ builder.addCase(deleteLabel.fulfilled, (state, action) => { labelAdapter.removeOne(state, action.payload); }); + builder.addCase(deleteBoard.fulfilled, (state) => { + labelAdapter.removeAll(state); + }); }, }); diff --git a/frontend/src/features/member/MemberSlice.tsx b/frontend/src/features/member/MemberSlice.tsx index 2288ccba..39fa9813 100644 --- a/frontend/src/features/member/MemberSlice.tsx +++ b/frontend/src/features/member/MemberSlice.tsx @@ -4,7 +4,7 @@ import { createEntityAdapter, } from "@reduxjs/toolkit"; import { BoardMember } from "types"; -import { fetchBoardById } from "features/board/BoardSlice"; +import { deleteBoard, fetchBoardById } from "features/board/BoardSlice"; import { RootState } from "store"; const memberAdapter = createEntityAdapter({ @@ -38,6 +38,9 @@ export const slice = createSlice({ builder.addCase(fetchBoardById.fulfilled, (state, action) => { memberAdapter.setAll(state, action.payload.members); }); + builder.addCase(deleteBoard.fulfilled, (state) => { + memberAdapter.removeAll(state); + }); }, }); diff --git a/frontend/src/features/task/TaskSlice.tsx b/frontend/src/features/task/TaskSlice.tsx index 04e2a1f6..2779e52c 100644 --- a/frontend/src/features/task/TaskSlice.tsx +++ b/frontend/src/features/task/TaskSlice.tsx @@ -1,6 +1,6 @@ import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit"; import { TasksByColumn, ITask, Id, NewTask, PriorityValue } from "types"; -import { fetchBoardById } from "features/board/BoardSlice"; +import { deleteBoard, fetchBoardById } from "features/board/BoardSlice"; import { AppDispatch, AppThunk, RootState } from "store"; import { createErrorToast, @@ -156,6 +156,10 @@ export const slice = createSlice({ ); } }); + builder.addCase(deleteBoard.fulfilled, (state) => { + state.byColumn = {}; + state.byId = {}; + }); }, });