From 335fbfe81c9d5a36b49929de0394408e9db46dd3 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Thu, 7 Aug 2025 09:19:30 +0400 Subject: [PATCH] feat: board creation and actions dropdown --- src/app/deals/components/Board/Board.tsx | 55 +++++++++++++++- src/app/deals/components/Boards/Boards.tsx | 22 ++++--- .../CreateBoardButton/CreateBoardButton.tsx | 54 +++++++++++++++ .../deals/components/DealCard/DealCard.tsx | 1 - .../components/SortableItem/SortableItem.tsx | 37 ----------- .../StatusColumnWrapper.tsx | 9 ++- src/app/deals/contexts/BoardsContext.tsx | 47 +++++++++++++- src/lib/client/@tanstack/react-query.gen.ts | 41 +++++++++++- src/lib/client/sdk.gen.ts | 25 ++++++- src/lib/client/types.gen.ts | 65 ++++++++++++++++++- src/lib/client/zod.gen.ts | 37 ++++++++++- src/utils/lexorank.ts | 9 +++ 12 files changed, 341 insertions(+), 61 deletions(-) create mode 100644 src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx delete mode 100644 src/app/deals/components/SortableItem/SortableItem.tsx diff --git a/src/app/deals/components/Board/Board.tsx b/src/app/deals/components/Board/Board.tsx index 7e29b3c..144fe93 100644 --- a/src/app/deals/components/Board/Board.tsx +++ b/src/app/deals/components/Board/Board.tsx @@ -1,5 +1,7 @@ -import React, { FC } from "react"; -import { Box } from "@mantine/core"; +import React, { FC, useState } 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 = { @@ -7,7 +9,54 @@ type Props = { }; const Board: FC = ({ board }) => { - return {board.name}; + const [isHovered, setIsHovered] = useState(false); + const { selectedBoard } = useBoardsContext(); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)}> + {board.name} + + + { + e.preventDefault(); + e.stopPropagation(); + }}> + + + + + + + + Переименовать + + + + + + Удалить + + + + + + ); }; export default Board; diff --git a/src/app/deals/components/Boards/Boards.tsx b/src/app/deals/components/Boards/Boards.tsx index b2c4bb8..70c3509 100644 --- a/src/app/deals/components/Boards/Boards.tsx +++ b/src/app/deals/components/Boards/Boards.tsx @@ -2,12 +2,13 @@ import React from "react"; import { useMutation } from "@tanstack/react-query"; -import { ScrollArea } from "@mantine/core"; +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 SortableDnd from "@/components/dnd/SortableDnd"; import { notifications } from "@/lib/notifications"; const Boards = () => { @@ -51,13 +52,16 @@ const Boards = () => { scrollbars={"x"} scrollbarSize={0} w={"100%"}> - + + + + ); }; diff --git a/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx b/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx new file mode 100644 index 0000000..23bb30e --- /dev/null +++ b/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; +import { IconCheck, IconPlus, IconX } from "@tabler/icons-react"; +import { Box, Group, TextInput } from "@mantine/core"; +import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; + +const CreateBoardButton = () => { + const { onCreateBoardClick } = useBoardsContext(); + const [isWriting, setIsWriting] = useState(false); + const [name, setName] = useState(""); + + const onStartCreating = () => { + setName(""); + setIsWriting(true); + }; + + const onCancelCreating = () => setIsWriting(false); + + const onCompleteCreating = () => { + if (name) { + onCreateBoardClick(name); + } + setIsWriting(false); + }; + + return ( + + {isWriting ? ( + + setName(e.target.value)} + onKeyDown={e => + e.key === "Enter" && onCompleteCreating() + } + /> + + + + + + + + ) : ( + + + + )} + + ); +}; + +export default CreateBoardButton; diff --git a/src/app/deals/components/DealCard/DealCard.tsx b/src/app/deals/components/DealCard/DealCard.tsx index d423232..872b5d0 100644 --- a/src/app/deals/components/DealCard/DealCard.tsx +++ b/src/app/deals/components/DealCard/DealCard.tsx @@ -6,7 +6,6 @@ type Props = { }; const DealCard = ({ deal }: Props) => { - console.log("deal"); return {deal.name}; }; diff --git a/src/app/deals/components/SortableItem/SortableItem.tsx b/src/app/deals/components/SortableItem/SortableItem.tsx deleted file mode 100644 index 25c3af1..0000000 --- a/src/app/deals/components/SortableItem/SortableItem.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; - -type Props = { - children: React.ReactNode; - id: string; -}; - -const SortableItem = ({ children, id }: Props) => { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0 : 1, - }; - - return ( -
- {children} -
- ); -}; - -export default SortableItem; diff --git a/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx b/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx index 30962e8..409f076 100644 --- a/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx +++ b/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx @@ -8,11 +8,16 @@ type Props = { children: ReactNode; }; -const StatusColumnWrapper = ({ status, children, isDragging = false }: Props) => { +const StatusColumnWrapper = ({ + status, + children, + isDragging = false, +}: Props) => { return ( >; refetchBoards: () => void; + onCreateBoardClick: (name: string) => void; }; const BoardsContext = createContext(undefined); const useBoardsContextState = () => { const { selectedProject: project } = useProjectsContext(); - const { boards, setBoards, refetch: refetchBoards } = useBoardsList({ projectId: project?.id }); + const { + boards, + setBoards, + refetch: refetchBoards, + } = useBoardsList({ projectId: project?.id }); const [selectedBoard, setSelectedBoard] = useState( null ); @@ -44,12 +54,45 @@ 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(), + }, + }, + }); + }; + return { boards, setBoards, selectedBoard, setSelectedBoard, refetchBoards, + onCreateBoardClick, }; }; diff --git a/src/lib/client/@tanstack/react-query.gen.ts b/src/lib/client/@tanstack/react-query.gen.ts index 1c78edb..c3e0142 100644 --- a/src/lib/client/@tanstack/react-query.gen.ts +++ b/src/lib/client/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getBoards, updateBoard, getDeals, updateDeal, getProjects, getStatuses, updateStatus } from '../sdk.gen'; +import { type Options, getBoards, createBoard, updateBoard, getDeals, updateDeal, getProjects, getStatuses, updateStatus } from '../sdk.gen'; import { queryOptions, type UseMutationOptions } from '@tanstack/react-query'; -import type { GetBoardsData, UpdateBoardData, UpdateBoardError, UpdateBoardResponse2, GetDealsData, UpdateDealData, UpdateDealError, UpdateDealResponse2, GetProjectsData, GetStatusesData, UpdateStatusData, UpdateStatusError, UpdateStatusResponse2 } from '../types.gen'; +import type { GetBoardsData, CreateBoardData, CreateBoardError, CreateBoardResponse2, UpdateBoardData, UpdateBoardError, UpdateBoardResponse2, GetDealsData, UpdateDealData, UpdateDealError, UpdateDealResponse2, GetProjectsData, GetStatusesData, UpdateStatusData, UpdateStatusError, UpdateStatusResponse2 } from '../types.gen'; import type { AxiosError } from 'axios'; import { client as _heyApiClient } from '../client.gen'; @@ -57,6 +57,43 @@ export const getBoardsOptions = (options: Options) => { }); }; +export const createBoardQueryKey = (options: Options) => createQueryKey('createBoard', options); + +/** + * Create Board + */ +export const createBoardOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await createBoard({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: createBoardQueryKey(options) + }); +}; + +/** + * Create Board + */ +export const createBoardMutation = (options?: Partial>): UseMutationOptions, Options> => { + const mutationOptions: UseMutationOptions, Options> = { + mutationFn: async (localOptions) => { + const { data } = await createBoard({ + ...options, + ...localOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + /** * Update Board */ diff --git a/src/lib/client/sdk.gen.ts b/src/lib/client/sdk.gen.ts index 157176a..3398095 100644 --- a/src/lib/client/sdk.gen.ts +++ b/src/lib/client/sdk.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from './client'; -import type { GetBoardsData, GetBoardsResponses, GetBoardsErrors, UpdateBoardData, UpdateBoardResponses, UpdateBoardErrors, GetDealsData, GetDealsResponses, GetDealsErrors, UpdateDealData, UpdateDealResponses, UpdateDealErrors, GetProjectsData, GetProjectsResponses, GetStatusesData, GetStatusesResponses, GetStatusesErrors, UpdateStatusData, UpdateStatusResponses, UpdateStatusErrors } from './types.gen'; -import { zGetBoardsData, zGetBoardsResponse2, zUpdateBoardData, zUpdateBoardResponse2, zGetDealsData, zGetDealsResponse2, zUpdateDealData, zUpdateDealResponse2, zGetProjectsData, zGetProjectsResponse2, zGetStatusesData, zGetStatusesResponse2, zUpdateStatusData, zUpdateStatusResponse2 } from './zod.gen'; +import type { GetBoardsData, GetBoardsResponses, GetBoardsErrors, CreateBoardData, CreateBoardResponses, CreateBoardErrors, UpdateBoardData, UpdateBoardResponses, UpdateBoardErrors, GetDealsData, GetDealsResponses, GetDealsErrors, UpdateDealData, UpdateDealResponses, UpdateDealErrors, GetProjectsData, GetProjectsResponses, GetStatusesData, GetStatusesResponses, GetStatusesErrors, UpdateStatusData, UpdateStatusResponses, UpdateStatusErrors } from './types.gen'; +import { zGetBoardsData, zGetBoardsResponse2, zCreateBoardData, zCreateBoardResponse2, zUpdateBoardData, zUpdateBoardResponse2, zGetDealsData, zGetDealsResponse2, zUpdateDealData, zUpdateDealResponse2, zGetProjectsData, zGetProjectsResponse2, zGetStatusesData, zGetStatusesResponse2, zUpdateStatusData, zUpdateStatusResponse2 } from './zod.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -36,6 +36,27 @@ export const getBoards = (options: Options }); }; +/** + * Create Board + */ +export const createBoard = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + requestValidator: async (data) => { + return await zCreateBoardData.parseAsync(data); + }, + responseType: 'json', + responseValidator: async (data) => { + return await zCreateBoardResponse2.parseAsync(data); + }, + url: '/board/', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + /** * Update Board */ diff --git a/src/lib/client/types.gen.ts b/src/lib/client/types.gen.ts index 56e8456..ff21a3e 100644 --- a/src/lib/client/types.gen.ts +++ b/src/lib/client/types.gen.ts @@ -4,14 +4,50 @@ * BoardSchema */ export type BoardSchema = { + /** + * Id + */ + id: number; /** * Name */ name: string; /** - * Id + * Lexorank */ - id: number; + lexorank: string; +}; + +/** + * CreateBoardRequest + */ +export type CreateBoardRequest = { + board: CreateBoardSchema; +}; + +/** + * CreateBoardResponse + */ +export type CreateBoardResponse = { + /** + * Message + */ + message: string; + board: BoardSchema; +}; + +/** + * CreateBoardSchema + */ +export type CreateBoardSchema = { + /** + * Name + */ + name: string; + /** + * Projectid + */ + projectId: number; /** * Lexorank */ @@ -267,6 +303,31 @@ export type GetBoardsResponses = { export type GetBoardsResponse2 = GetBoardsResponses[keyof GetBoardsResponses]; +export type CreateBoardData = { + body: CreateBoardRequest; + path?: never; + query?: never; + url: '/board/'; +}; + +export type CreateBoardErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateBoardError = CreateBoardErrors[keyof CreateBoardErrors]; + +export type CreateBoardResponses = { + /** + * Successful Response + */ + 200: CreateBoardResponse; +}; + +export type CreateBoardResponse2 = CreateBoardResponses[keyof CreateBoardResponses]; + export type UpdateBoardData = { body: UpdateBoardRequest; path: { diff --git a/src/lib/client/zod.gen.ts b/src/lib/client/zod.gen.ts index b7ebff6..6b1ac0d 100644 --- a/src/lib/client/zod.gen.ts +++ b/src/lib/client/zod.gen.ts @@ -6,11 +6,35 @@ import { z } from 'zod'; * BoardSchema */ export const zBoardSchema = z.object({ - name: z.string(), id: z.int(), + name: z.string(), lexorank: z.string() }); +/** + * CreateBoardSchema + */ +export const zCreateBoardSchema = z.object({ + name: z.string(), + projectId: z.int(), + lexorank: z.string() +}); + +/** + * CreateBoardRequest + */ +export const zCreateBoardRequest = z.object({ + board: zCreateBoardSchema +}); + +/** + * CreateBoardResponse + */ +export const zCreateBoardResponse = z.object({ + message: z.string(), + board: zBoardSchema +}); + /** * DealSchema */ @@ -186,6 +210,17 @@ export const zGetBoardsData = z.object({ */ export const zGetBoardsResponse2 = zGetBoardsResponse; +export const zCreateBoardData = z.object({ + body: zCreateBoardRequest, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful Response + */ +export const zCreateBoardResponse2 = zCreateBoardResponse; + export const zUpdateBoardData = z.object({ body: zUpdateBoardRequest, path: z.object({ diff --git a/src/utils/lexorank.ts b/src/utils/lexorank.ts index d201a46..6b70ae1 100644 --- a/src/utils/lexorank.ts +++ b/src/utils/lexorank.ts @@ -21,6 +21,15 @@ export function sortByLexorank(items: T[]): T[] { return items.sort(compareByLexorank); } +export function getMaxByLexorank( + items: T[] +): T | null { + return items.reduce( + (max, item) => (!max || item.lexorank > max.lexorank ? item : max), + null as T | null + ); +} + export function getNewLexorank( left?: LexoRank | null, right?: LexoRank | null