feat: project editor

This commit is contained in:
2025-08-30 15:31:42 +04:00
parent 1b97739063
commit b363554c46
12 changed files with 237 additions and 53 deletions

View File

@ -1,36 +0,0 @@
import { IconPlus } from "@tabler/icons-react";
import { ActionIcon, Box } from "@mantine/core";
import { modals } from "@mantine/modals";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import style from "./CreateProjectButton.module.css";
const CreateProjectButton = () => {
const { projectsCrud } = useProjectsContext();
const onCreateClick = () => {
modals.openContextModal({
modal: "enterNameModal",
title: "Создание проекта",
withCloseButton: true,
innerProps: {
onComplete: (name: string) => {
projectsCrud.onCreate(name);
},
},
});
};
return (
<Box>
<ActionIcon
variant={"default"}
onClick={onCreateClick}
radius="lg"
className={style.container}>
<IconPlus />
</ActionIcon>
</Box>
);
};
export default CreateProjectButton;

View File

@ -0,0 +1,26 @@
import { FC, PropsWithChildren } from "react";
import { ActionIcon, Box } from "@mantine/core";
import style from "./ProjectAction.module.css";
type Props = {
onClick: () => void;
};
const ProjectAction: FC<PropsWithChildren<Props>> = ({
onClick,
children,
}) => {
return (
<Box>
<ActionIcon
variant={"default"}
onClick={onClick}
radius="lg"
className={style.container}>
{children}
</ActionIcon>
</Box>
);
};
export default ProjectAction;

View File

@ -1,19 +1,43 @@
"use client";
import { IconEdit, IconPlus } from "@tabler/icons-react";
import { Flex, Group } from "@mantine/core";
import CreateProjectButton from "@/app/deals/components/desktop/CreateProjectButton/CreateProjectButton";
import { modals } from "@mantine/modals";
import ProjectAction from "@/app/deals/components/desktop/ProjectAction/ProjectAction";
import ViewSelector from "@/app/deals/components/desktop/ViewSelector/ViewSelector";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
import { useDrawersContext } from "@/drawers/DrawersContext";
import useIsMobile from "@/hooks/utils/useIsMobile";
const TopToolPanel = () => {
const { projects, setSelectedProjectId, selectedProject } =
const { projects, setSelectedProjectId, selectedProject, projectsCrud } =
useProjectsContext();
const { openDrawer } = useDrawersContext();
const isMobile = useIsMobile();
if (isMobile) return;
const onCreateClick = () => {
modals.openContextModal({
modal: "enterNameModal",
title: "Создание проекта",
withCloseButton: true,
innerProps: {
onComplete: projectsCrud.onCreate,
},
});
};
const onEditClick = () => {
if (!selectedProject) return;
openDrawer({
key: "selectedProjectEditorDrawer",
props: { project: selectedProject, projectsCrud },
});
};
return (
<Group justify={"space-between"}>
<ViewSelector />
@ -21,7 +45,12 @@ const TopToolPanel = () => {
wrap={"nowrap"}
align={"center"}
gap={"sm"}>
<CreateProjectButton />
<ProjectAction onClick={onEditClick}>
<IconEdit />
</ProjectAction>
<ProjectAction onClick={onCreateClick}>
<IconPlus />
</ProjectAction>
<ProjectSelect
data={projects}
value={selectedProject}

View File

@ -1,33 +1,33 @@
import { FC, useState } from "react";
import { isEqual } from "lodash";
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
import { Button, Group, Stack, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import { DealSchema } from "@/lib/client";
import { utcDateTimeToLocalString } from "@/utils/datetime";
import { ProjectsCrud } from "@/hooks/cruds/useProjectsCrud";
import { ProjectSchema } from "@/lib/client";
type Props = {
dealsCrud: DealsCrud;
deal: DealSchema;
projectsCrud: ProjectsCrud;
project: ProjectSchema;
onClose: () => void;
};
const GeneralTab: FC<Props> = ({ deal, dealsCrud, onClose }) => {
const [initialValues, setInitialValues] = useState(deal);
const form = useForm<DealSchema>({
const GeneralTab: FC<Props> = ({ project, projectsCrud, onClose }) => {
const [initialValues, setInitialValues] = useState(project);
const form = useForm<ProjectSchema>({
initialValues,
validate: {
name: value => !value && "Введите название",
},
});
const onSubmit = (values: DealSchema) => {
dealsCrud.onUpdate(deal.id, values);
const onSubmit = (values: ProjectSchema) => {
projectsCrud.onUpdate(project.id, values);
setInitialValues(values);
};
const onDelete = () => {
dealsCrud.onDelete(deal, onClose);
projectsCrud.onDelete(project, onClose);
};
return (
@ -37,7 +37,6 @@ const GeneralTab: FC<Props> = ({ deal, dealsCrud, onClose }) => {
label={"Название"}
{...form.getInputProps("name")}
/>
<Text>Создано: {utcDateTimeToLocalString(deal.createdAt)}</Text>
<Group
justify={"space-between"}
wrap={"nowrap"}>

View File

@ -0,0 +1,4 @@
.tab {
border-bottom-width: 3px;
}

View File

@ -0,0 +1,48 @@
"use client";
import React, { FC } from "react";
import { Drawer } from "@mantine/core";
import ProjectEditorBody from "@/app/deals/drawers/SelectedProjectEditorDrawer/components/ProjectEditorBody";
import { DrawerProps } from "@/drawers/types";
import { ProjectsCrud } from "@/hooks/cruds/useProjectsCrud";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ProjectSchema } from "@/lib/client";
type Props = {
project: ProjectSchema;
projectsCrud: ProjectsCrud;
};
const SelectedProjectEditorDrawer: FC<DrawerProps<Props>> = ({
opened,
onClose,
props,
}) => {
const isMobile = useIsMobile();
return (
<Drawer
size={isMobile ? "100%" : "40%"}
position={"right"}
onClose={onClose}
removeScrollProps={{ allowPinchZoom: true }}
withCloseButton={false}
opened={opened}
trapFocus={false}
styles={{
body: {
height: "100%",
display: "flex",
flexDirection: "column",
padding: 0,
},
}}>
<ProjectEditorBody
{...props}
onClose={onClose}
/>
</Drawer>
);
};
export default SelectedProjectEditorDrawer;

View File

@ -0,0 +1,71 @@
import { FC, useState } from "react";
import { isEqual } from "lodash";
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { DealsCrud } from "@/hooks/cruds/useDealsCrud";
import { DealSchema } from "@/lib/client";
import { utcDateTimeToLocalString } from "@/utils/datetime";
type Props = {
dealsCrud: DealsCrud;
deal: DealSchema;
onClose: () => void;
};
const GeneralTab: FC<Props> = ({ deal, dealsCrud, onClose }) => {
const [initialValues, setInitialValues] = useState(deal);
const form = useForm<DealSchema>({
initialValues,
validate: {
name: value => !value && "Введите название",
},
});
const onSubmit = (values: DealSchema) => {
dealsCrud.onUpdate(deal.id, values);
setInitialValues(values);
};
const onDelete = () => {
dealsCrud.onDelete(deal, onClose);
};
return (
<form onSubmit={form.onSubmit(onSubmit)}>
<Stack p={"md"}>
<TextInput
label={"Название"}
{...form.getInputProps("name")}
/>
<Text>Создано: {utcDateTimeToLocalString(deal.createdAt)}</Text>
<Group
justify={"space-between"}
wrap={"nowrap"}>
<Group wrap={"nowrap"}>
<Button
type={"submit"}
disabled={isEqual(form.values, initialValues)}
variant={"filled"}>
Сохранить
</Button>
<Button
type={"reset"}
onClick={() => form.reset()}
disabled={isEqual(form.values, initialValues)}
variant={"default"}>
Отменить
</Button>
</Group>
<Button
onClick={onDelete}
color={"red"}
variant={"outline"}>
Удалить
</Button>
</Group>
</Stack>
</form>
);
};
export default GeneralTab;

View File

@ -0,0 +1,38 @@
import { FC } from "react";
import { IconEdit } from "@tabler/icons-react";
import { Tabs } from "@mantine/core";
import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/components/GeneralTab";
import { ProjectsCrud } from "@/hooks/cruds/useProjectsCrud";
import { ProjectSchema } from "@/lib/client";
import styles from "../DealEditorDrawer.module.css";
type Props = {
projectsCrud: ProjectsCrud;
project: ProjectSchema;
onClose: () => void;
};
const ProjectEditorBody: FC<Props> = ({ projectsCrud, project, onClose }) => {
return (
<Tabs
defaultValue="general"
classNames={{ tab: styles.tab }}>
<Tabs.List>
<Tabs.Tab
value="general"
leftSection={<IconEdit />}>
Общая информация
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="general">
<GeneralTab
projectsCrud={projectsCrud}
project={project}
onClose={onClose}
/>
</Tabs.Panel>
</Tabs>
);
};
export default ProjectEditorBody;

View File

@ -0,0 +1,3 @@
import SelectedProjectEditorDrawer from "@/app/deals/drawers/SelectedProjectEditorDrawer/SelectedProjectEditorDrawer";
export default SelectedProjectEditorDrawer;

View File

@ -2,12 +2,14 @@ import BoardStatusesEditorDrawer from "@/app/deals/drawers/BoardStatusesEditorDr
import DealEditorDrawer from "@/app/deals/drawers/DealEditorDrawer/DealEditorDrawer";
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer";
import ProjectsEditorDrawer from "@/app/deals/drawers/ProjectsEditorDrawer";
import SelectedProjectEditorDrawer from "@/app/deals/drawers/SelectedProjectEditorDrawer";
const drawerRegistry = {
projectsEditorDrawer: ProjectsEditorDrawer,
boardStatusesEditorDrawer: BoardStatusesEditorDrawer,
projectBoardsEditorDrawer: ProjectBoardsEditorDrawer,
dealEditorDrawer: DealEditorDrawer,
selectedProjectEditorDrawer: SelectedProjectEditorDrawer,
};
export default drawerRegistry;

View File

@ -17,7 +17,7 @@ type Props = {
export type ProjectsCrud = {
onCreate: (name: string) => void;
onUpdate: (projectId: number, project: UpdateProjectSchema) => void;
onDelete: (project: ProjectSchema) => void;
onDelete: (project: ProjectSchema, onSuccess?: () => void) => void;
};
export const useProjectsCrud = ({ queryKey }: Props): ProjectsCrud => {