feat: drawers registry
This commit is contained in:
@ -13,7 +13,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Board: FC<Props> = ({ board }) => {
|
const Board: FC<Props> = ({ board }) => {
|
||||||
const { selectedBoard, onUpdateBoard } = useBoardsContext();
|
const { selectedBoard, onUpdateBoard, onDeleteBoard } = useBoardsContext();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@ -49,6 +49,7 @@ const Board: FC<Props> = ({ board }) => {
|
|||||||
isHovered={
|
isHovered={
|
||||||
selectedBoard?.id === board.id || isHovered
|
selectedBoard?.id === board.id || isHovered
|
||||||
}
|
}
|
||||||
|
onDeleteBoard={onDeleteBoard}
|
||||||
board={board}
|
board={board}
|
||||||
startEditing={startEditing}
|
startEditing={startEditing}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
import { Box, Group, Menu, Text } from "@mantine/core";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
board: BoardSchema;
|
board: BoardSchema;
|
||||||
startEditing: () => void;
|
startEditing: () => void;
|
||||||
|
onDeleteBoard: (board: BoardSchema) => void;
|
||||||
isHovered?: boolean;
|
isHovered?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardMenu: FC<Props> = ({ board, startEditing, isHovered = true }) => {
|
const BoardMenu: FC<Props> = ({
|
||||||
const { onDeleteBoard } = useBoardsContext();
|
board,
|
||||||
|
startEditing,
|
||||||
|
onDeleteBoard,
|
||||||
|
isHovered = true,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
|
|||||||
@ -6,17 +6,16 @@ import Boards from "@/app/deals/components/shared/Boards/Boards";
|
|||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||||
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
import useIsMobile from "@/hooks/useIsMobile";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const {
|
const { projects, setSelectedProjectId, refetchProjects, selectedProject } =
|
||||||
projects,
|
useProjectsContext();
|
||||||
setSelectedProjectId,
|
const { refetchBoards } = useBoardsContext();
|
||||||
selectedProject,
|
const { openDrawer } = useDrawersContext();
|
||||||
setIsEditorDrawerOpened: setIsProjectsDrawerOpened,
|
|
||||||
} = useProjectsContext();
|
|
||||||
const { setIsEditorDrawerOpened } = useBoardsContext();
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const getDesktopHeader = () => {
|
const getDesktopHeader = () => {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -45,19 +44,44 @@ const Header = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectProjectId = async (projectId: number | null) => {
|
||||||
|
await refetchProjects();
|
||||||
|
setSelectedProjectId(projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openProjectsEditorDrawer = () => {
|
||||||
|
openDrawer({
|
||||||
|
key: "projectsEditorDrawer",
|
||||||
|
props: {
|
||||||
|
setSelectedProjectId: selectProjectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openBoardsEditorDrawer = () => {
|
||||||
|
if (!selectedProject) return;
|
||||||
|
openDrawer({
|
||||||
|
key: "projectBoardsEditorDrawer",
|
||||||
|
props: {
|
||||||
|
project: selectedProject,
|
||||||
|
},
|
||||||
|
onClose: refetchBoards,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getMobileHeader = () => {
|
const getMobileHeader = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Group justify={"space-between"}>
|
<Group justify={"space-between"}>
|
||||||
<Box
|
<Box
|
||||||
p={"md"}
|
p={"md"}
|
||||||
onClick={() => setIsProjectsDrawerOpened(true)}>
|
onClick={openProjectsEditorDrawer}>
|
||||||
<IconChevronLeft />
|
<IconChevronLeft />
|
||||||
</Box>
|
</Box>
|
||||||
<Text>{selectedProject?.name}</Text>
|
<Text>{selectedProject?.name}</Text>
|
||||||
<Box
|
<Box
|
||||||
p={"md"}
|
p={"md"}
|
||||||
onClick={() => setIsEditorDrawerOpened(true)}>
|
onClick={openBoardsEditorDrawer}>
|
||||||
<IconSettings />
|
<IconSettings />
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Group, Text } from "@mantine/core";
|
import { Group, Text } from "@mantine/core";
|
||||||
import styles from "./StatusColumnHeader.module.css";
|
|
||||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
import { StatusSchema } from "@/lib/client";
|
import { StatusSchema } from "@/lib/client";
|
||||||
|
import styles from "./StatusColumnHeader.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
@ -12,7 +13,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||||
const { onUpdateStatus } = useStatusesContext();
|
const { onUpdateStatus, onDeleteStatus, refetchStatuses } =
|
||||||
|
useStatusesContext();
|
||||||
|
const { selectedBoard } = useBoardsContext();
|
||||||
|
|
||||||
const handleSave = (value: string) => {
|
const handleSave = (value: string) => {
|
||||||
const newValue = value.trim();
|
const newValue = value.trim();
|
||||||
@ -48,8 +51,11 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
|||||||
{status.name}
|
{status.name}
|
||||||
</Text>
|
</Text>
|
||||||
<StatusMenu
|
<StatusMenu
|
||||||
|
board={selectedBoard}
|
||||||
status={status}
|
status={status}
|
||||||
handleEdit={startEditing}
|
handleEdit={startEditing}
|
||||||
|
refetchStatuses={refetchStatuses}
|
||||||
|
onDeleteStatus={onDeleteStatus}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -6,26 +6,35 @@ import {
|
|||||||
IconTrash,
|
IconTrash,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
import { Box, Group, Menu, Text } from "@mantine/core";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
import useIsMobile from "@/hooks/useIsMobile";
|
||||||
import { StatusSchema } from "@/lib/client";
|
import { BoardSchema, StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
handleEdit: () => void;
|
handleEdit: () => void;
|
||||||
|
board: BoardSchema | null;
|
||||||
|
onDeleteStatus: (status: StatusSchema) => void;
|
||||||
|
refetchStatuses?: () => void;
|
||||||
|
withChangeOrderButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
const StatusMenu: FC<Props> = ({
|
||||||
|
status,
|
||||||
|
handleEdit,
|
||||||
|
board,
|
||||||
|
onDeleteStatus,
|
||||||
|
refetchStatuses,
|
||||||
|
withChangeOrderButton = true,
|
||||||
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { onDeleteStatus, setIsEditorDrawerOpened } = useStatusesContext();
|
const { openDrawer } = useDrawersContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{ cursor: "pointer" }}
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={e => e.stopPropagation()}>
|
onClick={e => e.stopPropagation()}>
|
||||||
<IconDotsVertical />
|
<IconDotsVertical />
|
||||||
</Box>
|
</Box>
|
||||||
@ -51,11 +60,18 @@ const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
|||||||
<Text>Удалить</Text>
|
<Text>Удалить</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{isMobile && (
|
{isMobile && withChangeOrderButton && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsEditorDrawerOpened(true);
|
if (!board) return;
|
||||||
|
openDrawer({
|
||||||
|
key: "boardStatusesEditorDrawer",
|
||||||
|
props: {
|
||||||
|
board,
|
||||||
|
},
|
||||||
|
onClose: refetchStatuses,
|
||||||
|
});
|
||||||
}}>
|
}}>
|
||||||
<Group wrap={"nowrap"}>
|
<Group wrap={"nowrap"}>
|
||||||
<IconExchange />
|
<IconExchange />
|
||||||
|
|||||||
@ -8,12 +8,11 @@ import { ProjectSchema, UpdateProjectSchema } from "@/lib/client";
|
|||||||
type ProjectsContextState = {
|
type ProjectsContextState = {
|
||||||
selectedProject: ProjectSchema | null;
|
selectedProject: ProjectSchema | null;
|
||||||
setSelectedProjectId: React.Dispatch<React.SetStateAction<number | null>>;
|
setSelectedProjectId: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
|
refetchProjects: () => Promise<void>;
|
||||||
projects: ProjectSchema[];
|
projects: ProjectSchema[];
|
||||||
onCreateProject: (name: string) => void;
|
onCreateProject: (name: string) => void;
|
||||||
onUpdateProject: (projectId: number, project: UpdateProjectSchema) => void;
|
onUpdateProject: (projectId: number, project: UpdateProjectSchema) => void;
|
||||||
onDeleteProject: (project: ProjectSchema) => void;
|
onDeleteProject: (project: ProjectSchema) => void;
|
||||||
isEditorDrawerOpened: boolean;
|
|
||||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
||||||
@ -21,8 +20,6 @@ const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const useProjectsContextState = () => {
|
const useProjectsContextState = () => {
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
const {
|
const {
|
||||||
projects,
|
projects,
|
||||||
setProjects,
|
setProjects,
|
||||||
@ -49,12 +46,11 @@ const useProjectsContextState = () => {
|
|||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
|
refetchProjects,
|
||||||
setSelectedProjectId,
|
setSelectedProjectId,
|
||||||
onCreateProject,
|
onCreateProject,
|
||||||
onUpdateProject,
|
onUpdateProject,
|
||||||
onDeleteProject,
|
onDeleteProject,
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, FC, useContext, useState } from "react";
|
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 { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import useStatusesList from "@/hooks/useStatusesList";
|
import useStatusesList from "@/hooks/useStatusesList";
|
||||||
import { useStatusesOperations } from "@/hooks/useStatusesOperations";
|
import { useStatusesOperations } from "@/hooks/useStatusesOperations";
|
||||||
import {
|
import { StatusSchema, UpdateStatusSchema } from "@/lib/client";
|
||||||
HttpValidationError,
|
|
||||||
Options,
|
|
||||||
StatusSchema,
|
|
||||||
UpdateStatusData,
|
|
||||||
UpdateStatusResponse,
|
|
||||||
UpdateStatusSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import { updateStatusMutation } from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
import { notifications } from "@/lib/notifications";
|
|
||||||
|
|
||||||
type StatusesContextState = {
|
type StatusesContextState = {
|
||||||
statuses: StatusSchema[];
|
statuses: StatusSchema[];
|
||||||
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
||||||
updateStatus: UseMutationResult<
|
|
||||||
UpdateStatusResponse,
|
|
||||||
AxiosError<HttpValidationError>,
|
|
||||||
Options<UpdateStatusData>
|
|
||||||
>;
|
|
||||||
refetchStatuses: () => void;
|
refetchStatuses: () => void;
|
||||||
onCreateStatus: (name: string) => void;
|
onCreateStatus: (name: string) => void;
|
||||||
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
|
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
|
||||||
onDeleteStatus: (status: StatusSchema) => void;
|
onDeleteStatus: (status: StatusSchema) => void;
|
||||||
isEditorDrawerOpened: boolean;
|
|
||||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusesContext = createContext<StatusesContextState | undefined>(
|
const StatusesContext = createContext<StatusesContextState | undefined>(
|
||||||
@ -46,19 +28,6 @@ const useStatusesContextState = () => {
|
|||||||
} = useStatusesList({
|
} = useStatusesList({
|
||||||
boardId: selectedBoard?.id,
|
boardId: selectedBoard?.id,
|
||||||
});
|
});
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
const updateStatus = useMutation({
|
|
||||||
...updateStatusMutation(),
|
|
||||||
onError: error => {
|
|
||||||
console.error(error);
|
|
||||||
notifications.error({
|
|
||||||
message: error.response?.data?.detail as string | undefined,
|
|
||||||
});
|
|
||||||
refetchStatuses();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { onCreateStatus, onUpdateStatus, onDeleteStatus } =
|
const { onCreateStatus, onUpdateStatus, onDeleteStatus } =
|
||||||
useStatusesOperations({
|
useStatusesOperations({
|
||||||
@ -71,13 +40,10 @@ const useStatusesContextState = () => {
|
|||||||
return {
|
return {
|
||||||
statuses,
|
statuses,
|
||||||
setStatuses,
|
setStatuses,
|
||||||
updateStatus,
|
|
||||||
refetchStatuses,
|
refetchStatuses,
|
||||||
onCreateStatus,
|
onCreateStatus,
|
||||||
onUpdateStatus,
|
onUpdateStatus,
|
||||||
onDeleteStatus,
|
onDeleteStatus,
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC } from "react";
|
||||||
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
import { Drawer, rem } from "@mantine/core";
|
||||||
import { Box, Center, Divider, Drawer, Group, rem, Text } from "@mantine/core";
|
import StatusesDrawerBody from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/StatusesDrawerBody";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { BoardStatusesContextProvider } from "@/app/deals/drawers/BoardStatusesEditorDrawer/contexts/BoardStatusesContext";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { DrawerProps } from "@/drawers/types";
|
||||||
import CreateStatusButton from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/CreateStatusButton";
|
import { BoardSchema } from "@/lib/client";
|
||||||
import StatusMobile from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/StatusMobile";
|
|
||||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
|
||||||
import { StatusSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const BoardStatusesEditorDrawer: FC = () => {
|
type Props = {
|
||||||
const {
|
board: BoardSchema;
|
||||||
statuses,
|
};
|
||||||
onUpdateStatus,
|
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
} = useStatusesContext();
|
|
||||||
const { selectedBoard } = useBoardsContext();
|
|
||||||
const onClose = () => setIsEditorDrawerOpened(false);
|
|
||||||
|
|
||||||
const renderDraggable = () => (
|
|
||||||
<Box p={"xs"}>
|
|
||||||
<IconGripVertical />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderStatus = (
|
|
||||||
status: StatusSchema,
|
|
||||||
renderDraggable?: (item: StatusSchema) => ReactNode
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
{renderDraggable && renderDraggable(status)}
|
|
||||||
<StatusMobile status={status} />
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
|
||||||
onUpdateStatus(itemId, { lexorank: newLexorank });
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const BoardStatusesEditorDrawer: FC<DrawerProps<Props>> = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
props: { board },
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
size={"100%"}
|
size={"100%"}
|
||||||
@ -49,7 +23,7 @@ const BoardStatusesEditorDrawer: FC = () => {
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
removeScrollProps={{ allowPinchZoom: true }}
|
removeScrollProps={{ allowPinchZoom: true }}
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
opened={isEditorDrawerOpened}
|
opened={opened}
|
||||||
trapFocus={false}
|
trapFocus={false}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
@ -59,27 +33,9 @@ const BoardStatusesEditorDrawer: FC = () => {
|
|||||||
gap: rem(10),
|
gap: rem(10),
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<Group justify={"space-between"}>
|
<BoardStatusesContextProvider board={board}>
|
||||||
<Box
|
<StatusesDrawerBody onClose={onClose} />
|
||||||
onClick={onClose}
|
</BoardStatusesContextProvider>
|
||||||
p={"xs"}>
|
|
||||||
<IconChevronLeft />
|
|
||||||
</Box>
|
|
||||||
<Center>
|
|
||||||
<Text>{selectedBoard?.name}</Text>
|
|
||||||
</Center>
|
|
||||||
<Box p={"lg"} />
|
|
||||||
</Group>
|
|
||||||
<Divider />
|
|
||||||
<SortableDnd
|
|
||||||
initialItems={statuses}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
renderItem={renderStatus}
|
|
||||||
renderDraggable={renderDraggable}
|
|
||||||
dragHandleStyle={{ width: "auto" }}
|
|
||||||
vertical
|
|
||||||
/>
|
|
||||||
<CreateStatusButton />
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
import { FC } from "react";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useBoardStatusesContext } from "@/app/deals/drawers/BoardStatusesEditorDrawer/contexts/BoardStatusesContext";
|
||||||
|
|
||||||
const CreateStatusButton = () => {
|
const CreateStatusButton: FC = () => {
|
||||||
const { onCreateStatus } = useStatusesContext();
|
const { onCreateStatus } = useBoardStatusesContext();
|
||||||
|
|
||||||
const onStartCreating = () => {
|
const onStartCreating = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import CreateProjectButton from "@/app/deals/drawers/ProjectsEditorDrawer/components/CreateProjectButton";
|
||||||
|
import ProjectMobile from "@/app/deals/drawers/ProjectsEditorDrawer/components/ProjectMobile";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setSelectedProjectId: (projectId: number | null) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectsDrawerBody: FC<Props> = ({ setSelectedProjectId, onClose }) => {
|
||||||
|
const { projects } = useProjectsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack gap={0}>
|
||||||
|
{projects.map((project, index) => (
|
||||||
|
<ProjectMobile
|
||||||
|
key={index}
|
||||||
|
project={project}
|
||||||
|
setSelectedProjectId={setSelectedProjectId}
|
||||||
|
closeDrawer={onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<CreateProjectButton />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectsDrawerBody;
|
||||||
@ -1,16 +1,17 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useBoardStatusesContext } from "@/app/deals/drawers/BoardStatusesEditorDrawer/contexts/BoardStatusesContext";
|
||||||
import { StatusSchema } from "@/lib/client";
|
import { BoardSchema, StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
|
board: BoardSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusMobile: FC<Props> = ({ status }) => {
|
const StatusMobile: FC<Props> = ({ status, board }) => {
|
||||||
const { onUpdateStatus } = useStatusesContext();
|
const { onUpdateStatus, onDeleteStatus } = useBoardStatusesContext();
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
@ -34,9 +35,12 @@ const StatusMobile: FC<Props> = ({ status }) => {
|
|||||||
<Box>
|
<Box>
|
||||||
<Text>{status.name}</Text>
|
<Text>{status.name}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<BoardMenu
|
<StatusMenu
|
||||||
board={status}
|
status={status}
|
||||||
startEditing={startEditing}
|
board={board}
|
||||||
|
onDeleteStatus={onDeleteStatus}
|
||||||
|
handleEdit={startEditing}
|
||||||
|
withChangeOrderButton={false}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
||||||
|
import { Box, Center, Divider, Group, Text } from "@mantine/core";
|
||||||
|
import CreateStatusButton from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/CreateStatusButton";
|
||||||
|
import StatusMobile from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/StatusMobile";
|
||||||
|
import { useBoardStatusesContext } from "@/app/deals/drawers/BoardStatusesEditorDrawer/contexts/BoardStatusesContext";
|
||||||
|
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||||
|
import { StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
|
||||||
|
const { onUpdateStatus, board, statuses } = useBoardStatusesContext();
|
||||||
|
|
||||||
|
const renderDraggable = () => (
|
||||||
|
<Box p={"xs"}>
|
||||||
|
<IconGripVertical />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStatus = (
|
||||||
|
status: StatusSchema,
|
||||||
|
renderDraggable?: (item: StatusSchema) => ReactNode
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Group wrap={"nowrap"}>
|
||||||
|
{renderDraggable && renderDraggable(status)}
|
||||||
|
<StatusMobile
|
||||||
|
status={status}
|
||||||
|
board={board}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (itemId: number, newLexorank: string) =>
|
||||||
|
onUpdateStatus(itemId, { lexorank: newLexorank });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group justify={"space-between"}>
|
||||||
|
<Box
|
||||||
|
onClick={onClose}
|
||||||
|
p={"xs"}>
|
||||||
|
<IconChevronLeft />
|
||||||
|
</Box>
|
||||||
|
<Center>
|
||||||
|
<Text>{board.name}</Text>
|
||||||
|
</Center>
|
||||||
|
<Box p={"lg"} />
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
<SortableDnd
|
||||||
|
initialItems={statuses}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
renderItem={renderStatus}
|
||||||
|
renderDraggable={renderDraggable}
|
||||||
|
dragHandleStyle={{ width: "auto" }}
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
<CreateStatusButton />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusesDrawerBody;
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, FC, useContext } from "react";
|
||||||
|
import useStatusesList from "@/hooks/useStatusesList";
|
||||||
|
import { useStatusesOperations } from "@/hooks/useStatusesOperations";
|
||||||
|
import { BoardSchema, StatusSchema, UpdateStatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type BoardStatusesContextState = {
|
||||||
|
board: BoardSchema;
|
||||||
|
statuses: StatusSchema[];
|
||||||
|
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
||||||
|
refetchStatuses: () => void;
|
||||||
|
onCreateStatus: (name: string) => void;
|
||||||
|
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
|
||||||
|
onDeleteStatus: (status: StatusSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardStatusesContext = createContext<
|
||||||
|
BoardStatusesContextState | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board: BoardSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useBoardStatusesContextState = ({ board }: Props) => {
|
||||||
|
const {
|
||||||
|
statuses,
|
||||||
|
setStatuses,
|
||||||
|
refetch: refetchStatuses,
|
||||||
|
} = useStatusesList({
|
||||||
|
boardId: board.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onCreateStatus, onUpdateStatus, onDeleteStatus } =
|
||||||
|
useStatusesOperations({
|
||||||
|
statuses,
|
||||||
|
setStatuses,
|
||||||
|
refetchStatuses,
|
||||||
|
boardId: board.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
board,
|
||||||
|
statuses,
|
||||||
|
setStatuses,
|
||||||
|
refetchStatuses,
|
||||||
|
onCreateStatus,
|
||||||
|
onUpdateStatus,
|
||||||
|
onDeleteStatus,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type BoardStatusesContextProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
} & Props;
|
||||||
|
|
||||||
|
export const BoardStatusesContextProvider: FC<
|
||||||
|
BoardStatusesContextProviderProps
|
||||||
|
> = ({ children, ...props }) => {
|
||||||
|
const state = useBoardStatusesContextState(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoardStatusesContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
</BoardStatusesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBoardStatusesContext = () => {
|
||||||
|
const context = useContext(BoardStatusesContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useBoardStatusesContext must be used within a BoardStatusesContextProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@ -1,47 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC } from "react";
|
||||||
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
import { Drawer, rem } from "@mantine/core";
|
||||||
import { Box, Center, Divider, Drawer, Group, rem, Text } from "@mantine/core";
|
import BoardsDrawerBody from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/BoardsDrawerBody";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { ProjectBoardsContextProvider } from "@/app/deals/drawers/ProjectBoardsEditorDrawer/contexts/ProjectBoardsContext";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { DrawerProps } from "@/drawers/types";
|
||||||
import BoardMobile from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/BoardMobile";
|
import { ProjectSchema } from "@/lib/client";
|
||||||
import CreateBoardButton from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/CreateBoardButton";
|
|
||||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const ProjectBoardsEditorDrawer: FC = () => {
|
type Props = {
|
||||||
const {
|
project: ProjectSchema;
|
||||||
boards,
|
};
|
||||||
onUpdateBoard,
|
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
} = useBoardsContext();
|
|
||||||
const { selectedProject } = useProjectsContext();
|
|
||||||
const onClose = () => setIsEditorDrawerOpened(false);
|
|
||||||
|
|
||||||
const renderDraggable = () => (
|
|
||||||
<Box p={"xs"}>
|
|
||||||
<IconGripVertical />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderBoard = (
|
|
||||||
board: BoardSchema,
|
|
||||||
renderDraggable?: (item: BoardSchema) => ReactNode
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
{renderDraggable && renderDraggable(board)}
|
|
||||||
<BoardMobile board={board} />
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
|
||||||
onUpdateBoard(itemId, { lexorank: newLexorank });
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const ProjectBoardsEditorDrawer: FC<DrawerProps<Props>> = ({
|
||||||
|
onClose,
|
||||||
|
opened,
|
||||||
|
props: { project },
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
size={"100%"}
|
size={"100%"}
|
||||||
@ -49,7 +23,7 @@ const ProjectBoardsEditorDrawer: FC = () => {
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
removeScrollProps={{ allowPinchZoom: true }}
|
removeScrollProps={{ allowPinchZoom: true }}
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
opened={isEditorDrawerOpened}
|
opened={opened}
|
||||||
trapFocus={false}
|
trapFocus={false}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
@ -59,27 +33,9 @@ const ProjectBoardsEditorDrawer: FC = () => {
|
|||||||
gap: rem(10),
|
gap: rem(10),
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<Group justify={"space-between"}>
|
<ProjectBoardsContextProvider project={project}>
|
||||||
<Box
|
<BoardsDrawerBody onClose={onClose} />
|
||||||
onClick={onClose}
|
</ProjectBoardsContextProvider>
|
||||||
p={"xs"}>
|
|
||||||
<IconChevronLeft />
|
|
||||||
</Box>
|
|
||||||
<Center>
|
|
||||||
<Text>{selectedProject?.name}</Text>
|
|
||||||
</Center>
|
|
||||||
<Box p={"lg"} />
|
|
||||||
</Group>
|
|
||||||
<Divider />
|
|
||||||
<SortableDnd
|
|
||||||
initialItems={boards}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
renderItem={renderBoard}
|
|
||||||
renderDraggable={renderDraggable}
|
|
||||||
dragHandleStyle={{ width: "auto" }}
|
|
||||||
vertical
|
|
||||||
/>
|
|
||||||
<CreateBoardButton />
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,16 +2,15 @@ import React, { FC } from "react";
|
|||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { BoardSchema, UpdateBoardSchema } from "@/lib/client";
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
board: BoardSchema;
|
board: BoardSchema;
|
||||||
|
onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void;
|
||||||
|
onDeleteBoard: (board: BoardSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardMobile: FC<Props> = ({ board }) => {
|
const BoardMobile: FC<Props> = ({ board, onUpdateBoard, onDeleteBoard }) => {
|
||||||
const { onUpdateBoard } = useBoardsContext();
|
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
modal: "enterNameModal",
|
modal: "enterNameModal",
|
||||||
@ -37,6 +36,7 @@ const BoardMobile: FC<Props> = ({ board }) => {
|
|||||||
<BoardMenu
|
<BoardMenu
|
||||||
board={board}
|
board={board}
|
||||||
startEditing={startEditing}
|
startEditing={startEditing}
|
||||||
|
onDeleteBoard={onDeleteBoard}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
||||||
|
import { Box, Center, Divider, Group, Text } from "@mantine/core";
|
||||||
|
import BoardMobile from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/BoardMobile";
|
||||||
|
import CreateBoardButton from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/CreateBoardButton";
|
||||||
|
import { useProjectBoardsContext } from "@/app/deals/drawers/ProjectBoardsEditorDrawer/contexts/ProjectBoardsContext";
|
||||||
|
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardsDrawerBody: FC<Props> = ({ onClose }) => {
|
||||||
|
const { boards, onUpdateBoard, onDeleteBoard, project, onCreateBoard } =
|
||||||
|
useProjectBoardsContext();
|
||||||
|
|
||||||
|
const renderDraggable = () => (
|
||||||
|
<Box p={"xs"}>
|
||||||
|
<IconGripVertical />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderBoard = (
|
||||||
|
board: BoardSchema,
|
||||||
|
renderDraggable?: (item: BoardSchema) => ReactNode
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Group wrap={"nowrap"}>
|
||||||
|
{renderDraggable && renderDraggable(board)}
|
||||||
|
<BoardMobile
|
||||||
|
board={board}
|
||||||
|
onDeleteBoard={onDeleteBoard}
|
||||||
|
onUpdateBoard={onUpdateBoard}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||||
|
onUpdateBoard(itemId, { lexorank: newLexorank });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group justify={"space-between"}>
|
||||||
|
<Box
|
||||||
|
onClick={onClose}
|
||||||
|
p={"xs"}>
|
||||||
|
<IconChevronLeft />
|
||||||
|
</Box>
|
||||||
|
<Center>
|
||||||
|
<Text>{project.name}</Text>
|
||||||
|
</Center>
|
||||||
|
<Box p={"lg"} />
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
<SortableDnd
|
||||||
|
initialItems={boards}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
renderItem={renderBoard}
|
||||||
|
renderDraggable={renderDraggable}
|
||||||
|
dragHandleStyle={{ width: "auto" }}
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
<CreateBoardButton onCreateBoard={onCreateBoard} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoardsDrawerBody;
|
||||||
@ -1,11 +1,13 @@
|
|||||||
|
import { FC } from "react";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
|
|
||||||
const CreateBoardButton = () => {
|
type Props = {
|
||||||
const { onCreateBoard } = useBoardsContext();
|
onCreateBoard: (name: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateBoardButton: FC<Props> = ({ onCreateBoard }) => {
|
||||||
const onStartCreating = () => {
|
const onStartCreating = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
modal: "enterNameModal",
|
modal: "enterNameModal",
|
||||||
@ -18,7 +20,9 @@ const CreateBoardButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group ml={"xs"} onClick={onStartCreating}>
|
<Group
|
||||||
|
ml={"xs"}
|
||||||
|
onClick={onStartCreating}>
|
||||||
<IconPlus />
|
<IconPlus />
|
||||||
<Box mt={5}>
|
<Box mt={5}>
|
||||||
<Text>Создать доску</Text>
|
<Text>Создать доску</Text>
|
||||||
|
|||||||
@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, FC, useContext } from "react";
|
||||||
|
import useBoardsList from "@/hooks/useBoardsList";
|
||||||
|
import { useBoardsOperations } from "@/hooks/useBoardsOperations";
|
||||||
|
import { BoardSchema, ProjectSchema, UpdateBoardSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type ProjectBoardsContextState = {
|
||||||
|
boards: BoardSchema[];
|
||||||
|
setBoards: React.Dispatch<React.SetStateAction<BoardSchema[]>>;
|
||||||
|
project: ProjectSchema;
|
||||||
|
refetchBoards: () => void;
|
||||||
|
onCreateBoard: (name: string) => void;
|
||||||
|
onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void;
|
||||||
|
onDeleteBoard: (board: BoardSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardsContext = createContext<
|
||||||
|
ProjectBoardsContextState | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
project: ProjectSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useProjectBoardsContextState = ({ project }: Props) => {
|
||||||
|
const {
|
||||||
|
boards,
|
||||||
|
setBoards,
|
||||||
|
refetch: refetchBoards,
|
||||||
|
} = useBoardsList({ projectId: project?.id });
|
||||||
|
|
||||||
|
const { onCreateBoard, onUpdateBoard, onDeleteBoard } = useBoardsOperations(
|
||||||
|
{
|
||||||
|
boards,
|
||||||
|
setBoards,
|
||||||
|
refetchBoards,
|
||||||
|
projectId: project?.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
boards,
|
||||||
|
setBoards,
|
||||||
|
project,
|
||||||
|
refetchBoards,
|
||||||
|
onCreateBoard,
|
||||||
|
onUpdateBoard,
|
||||||
|
onDeleteBoard,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectBoardsContextProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
} & Props;
|
||||||
|
|
||||||
|
export const ProjectBoardsContextProvider: FC<
|
||||||
|
ProjectBoardsContextProviderProps
|
||||||
|
> = ({ children, ...props }) => {
|
||||||
|
const state = useProjectBoardsContextState(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProjectBoardsContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
</ProjectBoardsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProjectBoardsContext = () => {
|
||||||
|
const context = useContext(ProjectBoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useProjectBoardsContext must be used within a ProjectBoardsContextProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@ -1,16 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Center, Divider, Drawer, rem, Stack, Text } from "@mantine/core";
|
import { Center, Divider, Drawer, rem, Text } from "@mantine/core";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import CreateProjectButton from "@/app/deals/drawers/ProjectsEditorDrawer/components/CreateProjectButton";
|
import ProjectsDrawerBody from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/ProjectsDrawerBody";
|
||||||
import ProjectMobile from "@/app/deals/drawers/ProjectsEditorDrawer/components/ProjectMobile";
|
import { DrawerProps } from "@/drawers/types";
|
||||||
|
|
||||||
const ProjectsEditorDrawer: FC = () => {
|
type Props = {
|
||||||
const { projects, isEditorDrawerOpened, setIsEditorDrawerOpened } =
|
setSelectedProjectId: (projectId: number | null) => void;
|
||||||
useProjectsContext();
|
};
|
||||||
const onClose = () => setIsEditorDrawerOpened(false);
|
|
||||||
|
|
||||||
|
const ProjectsEditorDrawer: FC<DrawerProps<Props>> = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
props: { setSelectedProjectId },
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
size={"100%"}
|
size={"100%"}
|
||||||
@ -18,7 +22,7 @@ const ProjectsEditorDrawer: FC = () => {
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
removeScrollProps={{ allowPinchZoom: true }}
|
removeScrollProps={{ allowPinchZoom: true }}
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
opened={isEditorDrawerOpened}
|
opened={opened}
|
||||||
trapFocus={false}
|
trapFocus={false}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
@ -32,15 +36,12 @@ const ProjectsEditorDrawer: FC = () => {
|
|||||||
<Text>Проекты</Text>
|
<Text>Проекты</Text>
|
||||||
</Center>
|
</Center>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack gap={0}>
|
<ProjectsContextProvider>
|
||||||
{projects.map((project, index) => (
|
<ProjectsDrawerBody
|
||||||
<ProjectMobile
|
setSelectedProjectId={setSelectedProjectId}
|
||||||
key={index}
|
onClose={onClose}
|
||||||
project={project}
|
|
||||||
/>
|
/>
|
||||||
))}
|
</ProjectsContextProvider>
|
||||||
</Stack>
|
|
||||||
<CreateProjectButton />
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
|
import { FC } from "react";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
|
||||||
const CreateProjectButton = () => {
|
const CreateProjectButton: FC = () => {
|
||||||
const { onCreateProject } = useProjectsContext();
|
const { onCreateProject } = useProjectsContext();
|
||||||
|
|
||||||
const onStartCreating = () => {
|
const onStartCreating = () => {
|
||||||
|
|||||||
@ -8,11 +8,16 @@ import styles from "./../ProjectsEditorDrawer.module.css";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
project: ProjectSchema;
|
project: ProjectSchema;
|
||||||
|
setSelectedProjectId: (projectId: number | null) => void;
|
||||||
|
closeDrawer: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectMobile: FC<Props> = ({ project }) => {
|
const ProjectMobile: FC<Props> = ({
|
||||||
const { onUpdateProject, setSelectedProjectId, setIsEditorDrawerOpened } =
|
project,
|
||||||
useProjectsContext();
|
setSelectedProjectId,
|
||||||
|
closeDrawer,
|
||||||
|
}) => {
|
||||||
|
const { onUpdateProject } = useProjectsContext();
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
@ -28,7 +33,7 @@ const ProjectMobile: FC<Props> = ({ project }) => {
|
|||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
setSelectedProjectId(project.id);
|
setSelectedProjectId(project.id);
|
||||||
setIsEditorDrawerOpened(false);
|
closeDrawer();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
|||||||
const swiperRef = useRef<SwiperRef>(null);
|
const swiperRef = useRef<SwiperRef>(null);
|
||||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||||
const { statuses, setStatuses, updateStatus } = useStatusesContext();
|
const { statuses, setStatuses, onUpdateStatus } = useStatusesContext();
|
||||||
const { deals, setDeals, updateDeal } = useDealsContext();
|
const { deals, setDeals, updateDeal } = useDealsContext();
|
||||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@ -229,16 +229,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
||||||
updateStatus.mutate({
|
onUpdateStatus(statusId, { lexorank });
|
||||||
path: {
|
|
||||||
statusId,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
status: {
|
|
||||||
lexorank,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
||||||
|
|||||||
@ -1,34 +1,28 @@
|
|||||||
|
import { Space } from "@mantine/core";
|
||||||
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
||||||
import Header from "@/app/deals/components/shared/Header/Header";
|
import Header from "@/app/deals/components/shared/Header/Header";
|
||||||
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
|
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
||||||
import BoardStatusesEditorDrawer from "@/app/deals/drawers/BoardStatusesEditorDrawer";
|
|
||||||
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer";
|
|
||||||
import ProjectsEditorDrawer from "@/app/deals/drawers/ProjectsEditorDrawer";
|
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||||
import { DealsContextProvider } from "./contexts/DealsContext";
|
import { DealsContextProvider } from "./contexts/DealsContext";
|
||||||
import { Space } from "@mantine/core";
|
|
||||||
|
|
||||||
export default function DealsPage() {
|
export default function DealsPage() {
|
||||||
return (
|
return (
|
||||||
<ProjectsContextProvider>
|
<ProjectsContextProvider>
|
||||||
<BoardsContextProvider>
|
<BoardsContextProvider>
|
||||||
|
<StatusesContextProvider>
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageBlock className={"mobile-margin-height"}>
|
<PageBlock className={"mobile-margin-height"}>
|
||||||
<Header />
|
<Header />
|
||||||
<Space h={"md"}/>
|
<Space h={"md"} />
|
||||||
<StatusesContextProvider>
|
|
||||||
<DealsContextProvider>
|
<DealsContextProvider>
|
||||||
<Funnel />
|
<Funnel />
|
||||||
</DealsContextProvider>
|
</DealsContextProvider>
|
||||||
<BoardStatusesEditorDrawer />
|
|
||||||
</StatusesContextProvider>
|
|
||||||
<ProjectBoardsEditorDrawer />
|
|
||||||
<ProjectsEditorDrawer />
|
|
||||||
</PageBlock>
|
</PageBlock>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
</StatusesContextProvider>
|
||||||
</BoardsContextProvider>
|
</BoardsContextProvider>
|
||||||
</ProjectsContextProvider>
|
</ProjectsContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import AppShellMainWrapper from "@/components/layout/AppShellWrappers/AppShellMa
|
|||||||
import AppShellNavbarWrapper from "@/components/layout/AppShellWrappers/AppShellNavbarWrapper";
|
import AppShellNavbarWrapper from "@/components/layout/AppShellWrappers/AppShellNavbarWrapper";
|
||||||
import Footer from "@/components/layout/Footer/Footer";
|
import Footer from "@/components/layout/Footer/Footer";
|
||||||
import Navbar from "@/components/layout/Navbar/Navbar";
|
import Navbar from "@/components/layout/Navbar/Navbar";
|
||||||
|
import { DrawersContextProvider } from "@/drawers/DrawersContext";
|
||||||
import { modals } from "@/modals/modals";
|
import { modals } from "@/modals/modals";
|
||||||
import { ReactQueryProvider } from "@/providers/ReactQueryProvider";
|
import { ReactQueryProvider } from "@/providers/ReactQueryProvider";
|
||||||
import ReduxProvider from "@/providers/ReduxProvider";
|
import ReduxProvider from "@/providers/ReduxProvider";
|
||||||
@ -64,6 +65,7 @@ export default function RootLayout({ children }: Props) {
|
|||||||
<ModalsProvider
|
<ModalsProvider
|
||||||
labels={{ confirm: "Да", cancel: "Нет" }}
|
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||||
modals={modals}>
|
modals={modals}>
|
||||||
|
<DrawersContextProvider>
|
||||||
<AppShell
|
<AppShell
|
||||||
layout={"alt"}
|
layout={"alt"}
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
@ -85,6 +87,7 @@ export default function RootLayout({ children }: Props) {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</AppShellFooterWrapper>
|
</AppShellFooterWrapper>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
</DrawersContextProvider>
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
<Notifications position="bottom-right" />
|
<Notifications position="bottom-right" />
|
||||||
|
|||||||
101
src/drawers/DrawersContext.tsx
Normal file
101
src/drawers/DrawersContext.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, FC, useContext, useState } from "react";
|
||||||
|
import drawerRegistry from "@/drawers/drawersRegistry";
|
||||||
|
import {
|
||||||
|
DrawerKey,
|
||||||
|
DrawersStackState,
|
||||||
|
OpenDrawerParams,
|
||||||
|
} from "@/drawers/types";
|
||||||
|
|
||||||
|
type DrawersContextState = {
|
||||||
|
stack: DrawersStackState[];
|
||||||
|
openDrawer: <TKey extends DrawerKey>(
|
||||||
|
params: OpenDrawerParams<TKey>
|
||||||
|
) => void;
|
||||||
|
closeDrawer: (key?: DrawerKey) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DrawersContext = createContext<DrawersContextState | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const useDrawersContextState = () => {
|
||||||
|
const [stack, setStack] = useState<DrawersStackState[]>([]);
|
||||||
|
|
||||||
|
const openDrawer = <TKey extends DrawerKey>(
|
||||||
|
params: OpenDrawerParams<TKey>
|
||||||
|
) => {
|
||||||
|
setStack(prev => {
|
||||||
|
const filtered = prev.filter(d => d.key !== params.key);
|
||||||
|
return [...filtered, { ...params, opened: false }];
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setStack(prev =>
|
||||||
|
prev.map(d =>
|
||||||
|
d.key === params.key ? { ...d, opened: true } : d
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDrawer = (key?: DrawerKey) => {
|
||||||
|
setStack(prev =>
|
||||||
|
prev.map(d => (d.key === key ? { ...d, opened: false } : d))
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setStack(prev =>
|
||||||
|
key ? prev.filter(d => d.key !== key) : prev.slice(0, -1)
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
stack,
|
||||||
|
openDrawer,
|
||||||
|
closeDrawer,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type BoardsContextProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DrawersContextProvider: FC<BoardsContextProviderProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const state = useDrawersContextState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawersContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{state.stack.map(({ key, props, onClose, opened }) => {
|
||||||
|
const Component = drawerRegistry[key];
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
key={key}
|
||||||
|
opened={opened || false}
|
||||||
|
onClose={() => {
|
||||||
|
state.closeDrawer(key);
|
||||||
|
onClose && onClose();
|
||||||
|
}}
|
||||||
|
props={props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DrawersContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDrawersContext = () => {
|
||||||
|
const context = useContext(DrawersContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useDrawersContext must be used within a DrawersContextProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
11
src/drawers/drawersRegistry.tsx
Normal file
11
src/drawers/drawersRegistry.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import BoardStatusesEditorDrawer from "@/app/deals/drawers/BoardStatusesEditorDrawer";
|
||||||
|
import ProjectsEditorDrawer from "@/app/deals/drawers/ProjectsEditorDrawer";
|
||||||
|
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer";
|
||||||
|
|
||||||
|
const drawerRegistry = {
|
||||||
|
projectsEditorDrawer: ProjectsEditorDrawer,
|
||||||
|
boardStatusesEditorDrawer: BoardStatusesEditorDrawer,
|
||||||
|
projectBoardsEditorDrawer: ProjectBoardsEditorDrawer,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default drawerRegistry;
|
||||||
24
src/drawers/types.ts
Normal file
24
src/drawers/types.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import drawerRegistry from "./drawersRegistry";
|
||||||
|
|
||||||
|
export type DrawerRegistry = typeof drawerRegistry;
|
||||||
|
|
||||||
|
export type DrawerKey = keyof DrawerRegistry;
|
||||||
|
|
||||||
|
export type DrawerProps<T = unknown> = {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
props: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DrawersStackState = {
|
||||||
|
key: DrawerKey;
|
||||||
|
props?: any;
|
||||||
|
onClose?: () => void;
|
||||||
|
opened?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OpenDrawerParams<TKey extends DrawerKey> = {
|
||||||
|
key: TKey;
|
||||||
|
props?: Parameters<DrawerRegistry[TKey]>[0]["props"];
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
@ -4,19 +4,24 @@ import { ProjectSchema } from "@/lib/client";
|
|||||||
import { getProjectsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
import { getProjectsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
const useProjectsList = () => {
|
const useProjectsList = () => {
|
||||||
const [projects, setProjects] = useState<ProjectSchema[]>([]);
|
const { refetch, data, isLoading } = useQuery({
|
||||||
|
|
||||||
const { data, refetch, isLoading } = useQuery({
|
|
||||||
...getProjectsOptions(),
|
...getProjectsOptions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [projects, setProjects] = useState<ProjectSchema[]>([]);
|
||||||
|
|
||||||
|
const refetchProjects = async () => {
|
||||||
|
const res = await refetch();
|
||||||
|
setProjects(res.data?.projects ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.projects) {
|
if (data?.projects) {
|
||||||
setProjects(data.projects);
|
setProjects(data.projects);
|
||||||
}
|
}
|
||||||
}, [data?.projects]);
|
}, [data?.projects]);
|
||||||
|
|
||||||
return { projects, setProjects, refetch, isLoading };
|
return { projects, setProjects, refetch: refetchProjects, isLoading };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useProjectsList;
|
export default useProjectsList;
|
||||||
|
|||||||
Reference in New Issue
Block a user