feat: status editing and deleting

This commit is contained in:
2025-08-07 15:46:11 +04:00
parent 7e2dd9763b
commit e29664ecc5
12 changed files with 616 additions and 69 deletions

View File

@ -4,14 +4,14 @@ import React, { FC, ReactNode } from "react";
import DealCard from "@/app/deals/components/DealCard/DealCard";
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
import StatusColumnWrapper from "@/app/deals/components/StatusColumnWrapper/StatusColumnWrapper";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
import { DealSchema, StatusSchema } from "@/lib/client";
import { sortByLexorank } from "@/utils/lexorank";
const Funnel: FC = () => {
const { deals } = useStatusesContext();
const { deals } = useDealsContext();
const {
sortedStatuses,

View File

@ -1,5 +1,7 @@
import React, { ReactNode } from "react";
import { Box, Text } from "@mantine/core";
import React, { ReactNode, useEffect, useRef, useState } from "react";
import { Box, Group, Text, TextInput } from "@mantine/core";
import StatusMenu from "@/app/deals/components/StatusColumnWrapper/StatusMenu";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import { StatusSchema } from "@/lib/client";
type Props = {
@ -13,23 +15,98 @@ const StatusColumnWrapper = ({
children,
isDragging = false,
}: Props) => {
const { onUpdateStatus } = useStatusesContext();
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(status.name);
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 handleEdit = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
setEditValue(status.name);
setIsEditing(true);
};
const handleSave = () => {
const newValue = editValue.trim();
if (newValue && newValue !== status.name) {
onUpdateStatus(status.id, { name: newValue });
}
setIsEditing(false);
};
return (
<Box
p={"xs"}
style={{
borderWidth: 1,
borderColor: "gray",
padding: 2,
width: "15vw",
minWidth: 150,
}}>
<Text
style={{
cursor: "grab",
userSelect: "none",
opacity: isDragging ? 0.5 : 1,
}}>
{status.name}
</Text>
<Group
wrap={"nowrap"}
justify={"space-between"}
ml={"xs"}
mb={"xs"}>
{isEditing ? (
<TextInput
ref={inputRef}
value={editValue}
onChange={e => setEditValue(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter") handleSave();
if (e.key === "Escape") {
setEditValue(status.name);
setIsEditing(false);
}
}}
variant="unstyled"
styles={{
input: {
height: 25,
minHeight: 25,
},
}}
/>
) : (
<Text
style={{
cursor: "grab",
userSelect: "none",
opacity: isDragging ? 0.5 : 1,
}}>
{status.name}
</Text>
)}
<StatusMenu
status={status}
handleEdit={handleEdit}
/>
</Group>
{children}
</Box>
);

View File

@ -0,0 +1,48 @@
import React, { FC } from "react";
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
import { Box, Group, Menu, Text } from "@mantine/core";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import { StatusSchema } from "@/lib/client";
type Props = {
status: StatusSchema;
handleEdit: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
};
const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
const { onDeleteStatus } = useStatusesContext();
return (
<Menu>
<Menu.Target>
<Box
p={5}
style={{
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={() => onDeleteStatus(status.id)}>
<Group wrap={"nowrap"}>
<IconTrash />
<Text>Удалить</Text>
</Group>
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};
export default StatusMenu;

View File

@ -0,0 +1,80 @@
"use client";
import React, { createContext, FC, useContext } from "react";
import { useMutation, UseMutationResult } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import useDealsList from "@/hooks/useDealsList";
import {
DealSchema,
HttpValidationError,
Options,
UpdateDealData,
UpdateDealResponse,
} from "@/lib/client";
import { updateDealMutation } from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
type DealsContextState = {
deals: DealSchema[];
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
updateDeal: UseMutationResult<
UpdateDealResponse,
AxiosError<HttpValidationError>,
Options<UpdateDealData>
>;
refetchDeals: () => void;
};
const DealsContext = createContext<DealsContextState | undefined>(undefined);
const useDealsContextState = () => {
const { selectedBoard } = useBoardsContext();
const {
deals,
setDeals,
refetch: refetchDeals,
} = useDealsList({ boardId: selectedBoard?.id });
const updateDeal = useMutation({
...updateDealMutation(),
onError: error => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
refetchDeals();
},
});
return {
deals,
setDeals,
updateDeal,
refetchDeals,
};
};
type DealsContextProviderProps = {
children: React.ReactNode;
};
export const DealsContextProvider: FC<DealsContextProviderProps> = ({
children,
}) => {
const state = useDealsContextState();
return (
<DealsContext.Provider value={state}>{children}</DealsContext.Provider>
);
};
export const useDealsContext = () => {
const context = useContext(DealsContext);
if (!context) {
throw new Error(
"useDealsContext must be used within a DealsContextProvider"
);
}
return context;
};

View File

@ -4,22 +4,17 @@ import React, { createContext, FC, useContext, useEffect } from "react";
import { useMutation, UseMutationResult } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import useDealsList from "@/hooks/useDealsList";
import useStatusesList from "@/hooks/useStatusesList";
import { useStatusesOperations } from "@/hooks/useStatusesOperations";
import {
DealSchema,
HttpValidationError,
Options,
StatusSchema,
UpdateDealData,
UpdateDealResponse,
UpdateStatusData,
UpdateStatusResponse,
UpdateStatusSchema,
} from "@/lib/client";
import {
updateDealMutation,
updateStatusMutation,
} from "@/lib/client/@tanstack/react-query.gen";
import { updateStatusMutation } from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
type StatusesContextState = {
@ -31,14 +26,9 @@ type StatusesContextState = {
Options<UpdateStatusData>
>;
refetchStatuses: () => void;
deals: DealSchema[];
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
updateDeal: UseMutationResult<
UpdateDealResponse,
AxiosError<HttpValidationError>,
Options<UpdateDealData>
>;
refetchDeals: () => void;
onCreateStatus: (name: string) => void;
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
onDeleteStatus: (statusId: number) => void;
};
const StatusesContext = createContext<StatusesContextState | undefined>(
@ -55,12 +45,6 @@ const useStatusesContextState = () => {
boardId: selectedBoard?.id,
});
const {
deals,
setDeals,
refetch: refetchDeals,
} = useDealsList({ boardId: selectedBoard?.id });
useEffect(() => {
refetchStatuses();
}, [selectedBoard]);
@ -76,26 +60,22 @@ const useStatusesContextState = () => {
},
});
const updateDeal = useMutation({
...updateDealMutation(),
onError: error => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
refetchDeals();
},
});
const { onCreateStatus, onUpdateStatus, onDeleteStatus } =
useStatusesOperations({
statuses,
setStatuses,
refetchStatuses,
boardId: selectedBoard?.id,
});
return {
statuses,
setStatuses,
updateStatus,
refetchStatuses,
deals,
setDeals,
updateDeal,
refetchDeals,
onCreateStatus,
onUpdateStatus,
onDeleteStatus,
};
};

View File

@ -1,6 +1,7 @@
import { useMemo, useState } from "react";
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
import { useDebouncedCallback } from "@mantine/hooks";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
@ -10,8 +11,8 @@ import { sortByLexorank } from "@/utils/lexorank";
const useDealsAndStatusesDnd = () => {
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
const { statuses, deals, setDeals, setStatuses, updateDeal, updateStatus } =
useStatusesContext();
const { statuses, setStatuses, updateStatus } = useStatusesContext();
const { deals, setDeals, updateDeal } = useDealsContext();
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
const {

View File

@ -7,6 +7,7 @@ import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
import PageBlock from "@/components/layout/PageBlock/PageBlock";
import PageContainer from "@/components/layout/PageContainer/PageContainer";
import { DealsContextProvider } from "./contexts/DealsContext";
export default function DealsPage() {
return (
@ -18,7 +19,9 @@ export default function DealsPage() {
<Boards />
<Divider my={"xl"} />
<StatusesContextProvider>
<Funnel />
<DealsContextProvider>
<Funnel />
</DealsContextProvider>
</StatusesContextProvider>
</BoardsContextProvider>
</ProjectsContextProvider>