feat: board name editing

This commit is contained in:
2025-08-07 12:31:00 +04:00
parent 41f8d19d49
commit 7e2dd9763b
6 changed files with 251 additions and 128 deletions

View File

@ -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<Props> = ({ 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<HTMLInputElement>(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<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
setEditValue(board.name);
setIsEditing(true);
};
return (
<Group
@ -22,39 +63,34 @@ const Board: FC<Props> = ({ board }) => {
style={{ borderWidth: 1 }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}>
<Text>{board.name}</Text>
<Menu>
<Menu.Target>
<Box
style={{
opacity:
isHovered || selectedBoard?.id === board.id
? 1
: 0,
cursor: "pointer",
}}
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}>
<IconDotsVertical size={16} />
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item>
<Group wrap={"nowrap"}>
<IconEdit />
<Text>Переименовать</Text>
</Group>
</Menu.Item>
<Menu.Item onClick={() => onDeleteBoardClick(board.id)}>
<Group wrap={"nowrap"}>
<IconTrash />
<Text>Удалить</Text>
</Group>
</Menu.Item>
</Menu.Dropdown>
</Menu>
{isEditing ? (
<TextInput
ref={inputRef}
value={editValue}
onChange={e => 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,
},
}}
/>
) : (
<Text>{board.name}</Text>
)}
<BoardMenu
isHovered={isHovered}
board={board}
handleEdit={handleEdit}
/>
</Group>
);
};

View File

@ -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<HTMLButtonElement, MouseEvent>) => void;
};
const BoardMenu: FC<Props> = ({ isHovered, board, handleEdit }) => {
const { selectedBoard, onDeleteBoard } = useBoardsContext();
return (
<Menu>
<Menu.Target>
<Box
style={{
opacity:
isHovered || selectedBoard?.id === board.id ? 1 : 0,
cursor: "pointer",
}}
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}>
<IconDotsVertical size={16} />
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={handleEdit}>
<Group wrap={"nowrap"}>
<IconEdit />
<Text>Переименовать</Text>
</Group>
</Menu.Item>
<Menu.Item onClick={() => onDeleteBoard(board.id)}>
<Group wrap={"nowrap"}>
<IconTrash />
<Text>Удалить</Text>
</Group>
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};
export default BoardMenu;

View File

@ -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 <Board board={board} />;
};
const onDragEnd = (itemId: number, newLexorank: string) => {
updateBoard.mutate({
path: {
boardId: itemId,
},
body: {
board: {
lexorank: newLexorank,
},
},
});
onUpdateBoard(itemId, { lexorank: newLexorank });
};
const selectBoard = (board: BoardSchema) => {

View File

@ -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<boolean>(false);
const [name, setName] = useState<string>("");
@ -17,7 +17,7 @@ const CreateBoardButton = () => {
const onCompleteCreating = () => {
if (name) {
onCreateBoardClick(name);
onCreateBoard(name);
}
setIsWriting(false);
};

View File

@ -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<React.SetStateAction<BoardSchema | null>>;
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<BoardsContextState | undefined>(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,
};
};