diff --git a/src/app/deals/components/Board/Board.tsx b/src/app/deals/components/Board/Board.tsx index b4d5a8e..fbda00c 100644 --- a/src/app/deals/components/Board/Board.tsx +++ b/src/app/deals/components/Board/Board.tsx @@ -1,6 +1,6 @@ -import React, { FC, useState } from "react"; -import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react"; -import { Box, Group, Menu, Text } from "@mantine/core"; +import React, { FC, useEffect, useRef, useState } from "react"; +import { Group, Text, TextInput } from "@mantine/core"; +import BoardMenu from "@/app/deals/components/Board/BoardMenu"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { BoardSchema } from "@/lib/client"; @@ -10,7 +10,48 @@ type Props = { const Board: FC = ({ board }) => { const [isHovered, setIsHovered] = useState(false); - const { selectedBoard, onDeleteBoardClick } = useBoardsContext(); + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(board.name); + const { onUpdateBoard } = useBoardsContext(); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditing]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + isEditing && + inputRef.current && + !inputRef.current.contains(event.target as Node) + ) { + handleSave(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + }, [isEditing, editValue]); + + const handleSave = () => { + const newValue = editValue.trim(); + if (newValue && newValue !== board.name) { + onUpdateBoard(board.id, { name: newValue }); + } + setIsEditing(false); + }; + + const handleEdit = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + setEditValue(board.name); + setIsEditing(true); + }; return ( = ({ board }) => { style={{ borderWidth: 1 }} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> - {board.name} - - - { - e.preventDefault(); - e.stopPropagation(); - }}> - - - - - - - - Переименовать - - - onDeleteBoardClick(board.id)}> - - - Удалить - - - - + {isEditing ? ( + setEditValue(e.target.value)} + onKeyDown={e => { + if (e.key === "Enter") handleSave(); + if (e.key === "Escape") { + setEditValue(board.name); + setIsEditing(false); + } + }} + variant="unstyled" + styles={{ + input: { + height: 25, + minHeight: 25, + }, + }} + /> + ) : ( + {board.name} + )} + ); }; diff --git a/src/app/deals/components/Board/BoardMenu.tsx b/src/app/deals/components/Board/BoardMenu.tsx new file mode 100644 index 0000000..fa470d3 --- /dev/null +++ b/src/app/deals/components/Board/BoardMenu.tsx @@ -0,0 +1,50 @@ +import React, { FC } from "react"; +import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react"; +import { Box, Group, Menu, Text } from "@mantine/core"; +import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; +import { BoardSchema } from "@/lib/client"; + +type Props = { + isHovered: boolean; + board: BoardSchema; + handleEdit: (e: React.MouseEvent) => void; +}; + +const BoardMenu: FC = ({ isHovered, board, handleEdit }) => { + const { selectedBoard, onDeleteBoard } = useBoardsContext(); + + return ( + + + { + e.preventDefault(); + e.stopPropagation(); + }}> + + + + + + + + Переименовать + + + onDeleteBoard(board.id)}> + + + Удалить + + + + + ); +}; + +export default BoardMenu; diff --git a/src/app/deals/components/Boards/Boards.tsx b/src/app/deals/components/Boards/Boards.tsx index 70c3509..04626d7 100644 --- a/src/app/deals/components/Boards/Boards.tsx +++ b/src/app/deals/components/Boards/Boards.tsx @@ -1,45 +1,22 @@ "use client"; import React from "react"; -import { useMutation } from "@tanstack/react-query"; import { Group, ScrollArea } from "@mantine/core"; import Board from "@/app/deals/components/Board/Board"; import CreateBoardButton from "@/app/deals/components/CreateBoardButton/CreateBoardButton"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import SortableDnd from "@/components/dnd/SortableDnd"; import { BoardSchema } from "@/lib/client"; -import { updateBoardMutation } from "@/lib/client/@tanstack/react-query.gen"; -import { notifications } from "@/lib/notifications"; const Boards = () => { - const { boards, setSelectedBoard, refetchBoards } = useBoardsContext(); - - const updateBoard = useMutation({ - ...updateBoardMutation(), - onError: error => { - console.error(error); - notifications.error({ - message: error.response?.data?.detail as string | undefined, - }); - refetchBoards(); - }, - }); + const { boards, setSelectedBoard, onUpdateBoard } = useBoardsContext(); const renderBoard = (board: BoardSchema) => { return ; }; const onDragEnd = (itemId: number, newLexorank: string) => { - updateBoard.mutate({ - path: { - boardId: itemId, - }, - body: { - board: { - lexorank: newLexorank, - }, - }, - }); + onUpdateBoard(itemId, { lexorank: newLexorank }); }; const selectBoard = (board: BoardSchema) => { diff --git a/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx b/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx index 23bb30e..6a771bf 100644 --- a/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx +++ b/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx @@ -4,7 +4,7 @@ import { Box, Group, TextInput } from "@mantine/core"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; const CreateBoardButton = () => { - const { onCreateBoardClick } = useBoardsContext(); + const { onCreateBoard } = useBoardsContext(); const [isWriting, setIsWriting] = useState(false); const [name, setName] = useState(""); @@ -17,7 +17,7 @@ const CreateBoardButton = () => { const onCompleteCreating = () => { if (name) { - onCreateBoardClick(name); + onCreateBoard(name); } setIsWriting(false); }; diff --git a/src/app/deals/contexts/BoardsContext.tsx b/src/app/deals/contexts/BoardsContext.tsx index ab6e329..cfc010f 100644 --- a/src/app/deals/contexts/BoardsContext.tsx +++ b/src/app/deals/contexts/BoardsContext.tsx @@ -7,17 +7,10 @@ import React, { useEffect, useState, } from "react"; -import { useMutation } from "@tanstack/react-query"; -import { LexoRank } from "lexorank"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import useBoardsList from "@/hooks/useBoardsList"; -import { BoardSchema } from "@/lib/client"; -import { - createBoardMutation, - deleteBoardMutation, -} from "@/lib/client/@tanstack/react-query.gen"; -import { notifications } from "@/lib/notifications"; -import { getMaxByLexorank, getNewLexorank } from "@/utils/lexorank"; +import { useBoardsOperations } from "@/hooks/useBoardsOperations"; +import { BoardSchema, UpdateBoardSchema } from "@/lib/client"; type BoardsContextState = { boards: BoardSchema[]; @@ -25,8 +18,9 @@ type BoardsContextState = { selectedBoard: BoardSchema | null; setSelectedBoard: React.Dispatch>; refetchBoards: () => void; - onCreateBoardClick: (name: string) => void; - onDeleteBoardClick: (boardId: number) => void; + onCreateBoard: (name: string) => void; + onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void; + onDeleteBoard: (boardId: number) => void; }; const BoardsContext = createContext(undefined); @@ -58,57 +52,14 @@ const useBoardsContextState = () => { } }, [boards]); - const createBoard = useMutation({ - ...createBoardMutation(), - onError: error => { - console.error(error); - notifications.error({ - message: error.response?.data?.detail as string | undefined, - }); - refetchBoards(); - }, - onSuccess: res => { - setBoards([...boards, res.board]); - }, - }); - - const onCreateBoardClick = (name: string) => { - if (!project) return; - const lastBoard = getMaxByLexorank(boards); - const newLexorank = getNewLexorank( - lastBoard ? LexoRank.parse(lastBoard.lexorank) : null - ); - - createBoard.mutate({ - body: { - board: { - name, - projectId: project.id, - lexorank: newLexorank.toString(), - }, - }, - }); - }; - - const deleteBoard = useMutation({ - ...deleteBoardMutation(), - onError: error => { - console.error(error); - notifications.error({ - message: error.response?.data?.detail as string | undefined, - }); - refetchBoards(); - }, - }); - - const onDeleteBoardClick = (boardId: number) => { - deleteBoard.mutate({ - path: { - boardId, - }, - }); - setBoards(boards => boards.filter(board => board.id !== boardId)); - }; + const { onCreateBoard, onUpdateBoard, onDeleteBoard } = useBoardsOperations( + { + boards, + setBoards, + refetchBoards, + projectId: project?.id, + } + ); return { boards, @@ -116,8 +67,9 @@ const useBoardsContextState = () => { selectedBoard, setSelectedBoard, refetchBoards, - onCreateBoardClick, - onDeleteBoardClick, + onCreateBoard, + onUpdateBoard, + onDeleteBoard, }; }; diff --git a/src/hooks/useBoardsOperations.ts b/src/hooks/useBoardsOperations.ts new file mode 100644 index 0000000..2aeb5e3 --- /dev/null +++ b/src/hooks/useBoardsOperations.ts @@ -0,0 +1,108 @@ +import React from "react"; +import { useMutation } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import { LexoRank } from "lexorank"; +import { + BoardSchema, + HttpValidationError, + UpdateBoardSchema, +} from "@/lib/client"; +import { + createBoardMutation, + deleteBoardMutation, + updateBoardMutation, +} from "@/lib/client/@tanstack/react-query.gen"; +import { notifications } from "@/lib/notifications"; +import { getMaxByLexorank, getNewLexorank } from "@/utils/lexorank"; + +type UseBoardsOperationsProps = { + boards: BoardSchema[]; + setBoards: React.Dispatch>; + refetchBoards: () => void; + projectId?: number; +}; + +export const useBoardsOperations = ({ + boards, + setBoards, + refetchBoards, + projectId, +}: UseBoardsOperationsProps) => { + const onError = (error: AxiosError) => { + console.error(error); + notifications.error({ + message: error.response?.data?.detail as string | undefined, + }); + refetchBoards(); + }; + + const createBoard = useMutation({ + ...createBoardMutation(), + onError, + onSuccess: res => { + setBoards([...boards, res.board]); + }, + }); + + const updateBoard = useMutation({ + ...updateBoardMutation(), + onError, + }); + + const deleteBoard = useMutation({ + ...deleteBoardMutation(), + onError, + }); + + const onCreateBoard = (name: string) => { + if (!projectId) return; + const lastBoard = getMaxByLexorank(boards); + const newLexorank = getNewLexorank( + lastBoard ? LexoRank.parse(lastBoard.lexorank) : null + ); + + createBoard.mutate({ + body: { + board: { + name, + projectId, + lexorank: newLexorank.toString(), + }, + }, + }); + }; + + const onUpdateBoard = (boardId: number, board: UpdateBoardSchema) => { + updateBoard.mutate({ + path: { boardId }, + body: { board }, + }); + + setBoards(boards => + boards.map(oldBoard => + oldBoard.id !== boardId + ? oldBoard + : { + id: oldBoard.id, + name: board.name ? board.name : oldBoard.name, + lexorank: board.lexorank + ? board.lexorank + : oldBoard.lexorank, + } + ) + ); + }; + + const onDeleteBoard = (boardId: number) => { + deleteBoard.mutate({ + path: { boardId }, + }); + setBoards(boards => boards.filter(board => board.id !== boardId)); + }; + + return { + onCreateBoard, + onUpdateBoard, + onDeleteBoard, + }; +};