Skip to content

Commit 784eeb0

Browse files
feat: project deletion and renaming (#217)
1 parent a53d41e commit 784eeb0

File tree

14 files changed

+540
-87
lines changed

14 files changed

+540
-87
lines changed

apps/console/src/app/components/environments/DeleteEnvironmentModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const DeleteEnvironmentModal = ({
3636

3737
return (
3838
<Modal
39-
title="Are You Sure?"
39+
title="Are you sure?"
4040
open={environmentToDelete !== null}
4141
onCancel={onCancel}
4242
okType="danger"

apps/console/src/app/components/projects/CreateNewProjectModal.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback } from "react";
33
import { useCreateProjectMutation } from "../../graphql/hooks/mutations";
44
import { useCurrentOrganization } from "../../lib/hooks/useCurrentOrganization";
55
import { trackEvent } from "../../lib/utils/analytics";
6-
import { tr } from "date-fns/locale";
6+
import { useNavigate } from "react-router-dom";
77

88
interface Props {
99
open: boolean;
@@ -14,6 +14,7 @@ interface Props {
1414
export const CreateNewProjectModal = ({ open, onClose, onCreated }: Props) => {
1515
const [form] = Form.useForm<{ projectName: string }>();
1616
const { organization } = useCurrentOrganization();
17+
const navigate = useNavigate();
1718
const {
1819
mutateAsync: createProject,
1920
error,
@@ -27,14 +28,21 @@ export const CreateNewProjectModal = ({ open, onClose, onCreated }: Props) => {
2728
const { token } = theme.useToken();
2829

2930
const handleCreateProject = useCallback(async () => {
30-
void createProject({
31-
name: form.getFieldValue("projectName"),
32-
organizationId: organization.id,
33-
}).catch(() => {
34-
form.resetFields();
35-
});
31+
createProject(
32+
{
33+
name: form.getFieldValue("projectName"),
34+
organizationId: organization.id,
35+
},
36+
{
37+
onSuccess: (data) => {
38+
onCreated();
39+
navigate(`/projects/${data.createProject.id}`);
40+
},
41+
}
42+
);
43+
3644
trackEvent("project_form_submitted");
37-
}, [createProject, form, organization.id]);
45+
}, [createProject, onCreated, form, organization.id, navigate]);
3846

3947
const onCancel = () => {
4048
onClose();
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Alert, Modal, Typography } from "antd";
2+
import { GetProjectsQuery } from "../../../@generated/graphql/graphql";
3+
import { useDeleteProjectMutation } from "../../graphql/hooks/mutations";
4+
import { trackEvent } from "../../lib/utils/analytics";
5+
6+
interface Props {
7+
projectToDelete: GetProjectsQuery["projects"][0] | null;
8+
onClose: () => void;
9+
onDelete: () => void;
10+
}
11+
12+
export const DeleteProjectModal = ({
13+
projectToDelete,
14+
onClose,
15+
onDelete,
16+
}: Props) => {
17+
const { mutate: deleteProject, error } = useDeleteProjectMutation();
18+
19+
const handleDelete = () => {
20+
deleteProject(
21+
{ id: projectToDelete.id },
22+
{
23+
onSuccess: () => {
24+
onDelete();
25+
},
26+
}
27+
);
28+
29+
trackEvent("project_delete_confirmed", {
30+
name: projectToDelete?.name,
31+
});
32+
};
33+
34+
const onCancel = () => {
35+
onClose();
36+
trackEvent("project_delete_cancelled", {
37+
name: projectToDelete?.name,
38+
});
39+
};
40+
41+
return (
42+
<Modal
43+
title="Are you sure?"
44+
open={projectToDelete !== null}
45+
onCancel={onCancel}
46+
okType="danger"
47+
okText="Delete"
48+
onOk={handleDelete}
49+
>
50+
{error && (
51+
<Alert type="error" message={error.response.errors[0].message} />
52+
)}
53+
<p>
54+
Are you sure you want to delete the{" "}
55+
<Typography.Text style={{ fontWeight: 800 }}>
56+
{projectToDelete?.name}
57+
</Typography.Text>{" "}
58+
project? All associated data will be lost.
59+
</p>
60+
</Modal>
61+
);
62+
};
Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,117 @@
1-
import { ArrowRightCircleIcon } from "@heroicons/react/24/outline";
2-
import { Card, Row, Typography } from "antd";
1+
import { Card, MenuProps, Row, Typography } from "antd";
32
import { useNavigate } from "react-router-dom";
43
import { trackEvent } from "../../lib/utils/analytics";
54
import Icon from "@ant-design/icons/lib/components/Icon";
5+
import { EllipsisVerticalIcon } from "@heroicons/react/24/solid";
6+
import Dropdown from "antd/es/dropdown/dropdown";
7+
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
8+
import { DeleteProjectModal } from "./DeleteProjectModal";
9+
import { useState } from "react";
10+
import { GetProjectsQuery } from "../../../@generated/graphql/graphql";
11+
import { RenameProjectModal } from "./RenameProjectModal";
612

713
interface ProjectCardProps {
8-
name: string;
9-
slug: string;
10-
id: string;
14+
project: GetProjectsQuery["projects"][0];
15+
onDelete: () => void;
16+
onUpdate: () => void;
1117
}
12-
export const ProjectCard = ({ name, slug, id }: ProjectCardProps) => {
18+
19+
export const ProjectCard = ({
20+
project,
21+
onDelete,
22+
onUpdate,
23+
}: ProjectCardProps) => {
24+
const { name, id } = project;
25+
26+
const [projectToDelete, setProjectToDelete] = useState<
27+
GetProjectsQuery["projects"][0] | null
28+
>(null);
29+
const [projectToRename, setProjectToRename] = useState<
30+
GetProjectsQuery["projects"][0] | null
31+
>(null);
1332
const navigate = useNavigate();
1433
const onCardClick = () => {
1534
navigate(`/projects/${id}`);
1635
trackEvent("project_nav_clicked", { projectId: id });
1736
};
1837

38+
const menuItems: MenuProps["items"] = [
39+
{
40+
label: "Rename",
41+
key: "rename",
42+
icon: <EditOutlined />,
43+
},
44+
{
45+
label: <Typography.Text type="danger">Delete</Typography.Text>,
46+
key: "delete",
47+
icon: (
48+
<Typography.Text type="danger">
49+
<DeleteOutlined />
50+
</Typography.Text>
51+
),
52+
},
53+
];
54+
55+
const handleMenuItemClick: MenuProps["onClick"] = (e) => {
56+
const { domEvent } = e;
57+
58+
switch (e.key) {
59+
case "rename":
60+
trackEvent("project_rename_modal_opened", { projectId: project.id });
61+
setProjectToRename(project);
62+
break;
63+
case "delete":
64+
trackEvent("project_delete_modal_opened", { projectId: project.id });
65+
setProjectToDelete(project);
66+
break;
67+
}
68+
domEvent.stopPropagation();
69+
};
70+
1971
return (
20-
<Card
21-
hoverable
22-
onClick={onCardClick}
23-
style={{ marginBottom: 16, height: 122 }}
24-
>
25-
<Row justify="space-between" align="middle">
26-
<Typography.Title level={4} style={{ margin: 0 }}>
27-
{name}
28-
</Typography.Title>
29-
30-
<Icon
31-
component={() => <ArrowRightCircleIcon height={24} opacity={0.5} />}
32-
/>
33-
</Row>
34-
</Card>
72+
<>
73+
<DeleteProjectModal
74+
projectToDelete={projectToDelete}
75+
onClose={() => setProjectToDelete(null)}
76+
onDelete={() => {
77+
onDelete();
78+
setProjectToDelete(null);
79+
}}
80+
/>
81+
<RenameProjectModal
82+
projectToRename={projectToRename}
83+
onClose={() => setProjectToRename(null)}
84+
onRename={() => {
85+
onUpdate();
86+
setProjectToRename(null);
87+
}}
88+
/>
89+
<Card
90+
hoverable
91+
onClick={onCardClick}
92+
style={{ marginBottom: 16, height: 122 }}
93+
>
94+
<Row justify="space-between" align="middle">
95+
<Typography.Title level={4} style={{ margin: 0 }}>
96+
{name}
97+
</Typography.Title>
98+
99+
<Dropdown
100+
trigger={["click"]}
101+
menu={{
102+
items: menuItems,
103+
onClick: handleMenuItemClick,
104+
}}
105+
>
106+
<Icon
107+
onClick={(e) => e.stopPropagation()}
108+
component={() => (
109+
<EllipsisVerticalIcon height={24} opacity={0.5} />
110+
)}
111+
/>
112+
</Dropdown>
113+
</Row>
114+
</Card>
115+
</>
35116
);
36117
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Alert, Form, Input, Modal, Space, Typography, theme } from "antd";
2+
import { GetProjectsQuery } from "../../../@generated/graphql/graphql";
3+
import { useUpdateProjectSettingsMutation } from "../../graphql/hooks/mutations";
4+
import { trackEvent } from "../../lib/utils/analytics";
5+
import { useEffect } from "react";
6+
7+
interface Props {
8+
projectToRename: GetProjectsQuery["projects"][0] | null;
9+
onClose: () => void;
10+
onRename: () => void;
11+
}
12+
13+
export const RenameProjectModal = ({
14+
projectToRename,
15+
onClose,
16+
onRename,
17+
}: Props) => {
18+
const [form] = Form.useForm<{ name: string }>();
19+
const { token } = theme.useToken();
20+
const { mutate: updateProjectSettings, error } =
21+
useUpdateProjectSettingsMutation();
22+
23+
useEffect(() => {
24+
form.resetFields();
25+
}, [projectToRename, form]);
26+
27+
const handleRename = () => {
28+
updateProjectSettings(
29+
{ projectId: projectToRename.id, name: form.getFieldValue("name") },
30+
{
31+
onSuccess: () => {
32+
onRename();
33+
},
34+
}
35+
);
36+
37+
trackEvent("project_rename_submitted", {
38+
name: projectToRename?.name,
39+
});
40+
};
41+
42+
const onCancel = () => {
43+
onClose();
44+
trackEvent("project_rename_cancelled", {
45+
name: projectToRename?.name,
46+
});
47+
};
48+
49+
return (
50+
<Modal
51+
title="Rename Project"
52+
open={projectToRename !== null}
53+
onCancel={onCancel}
54+
okText="Rename"
55+
okButtonProps={{
56+
form: "rename-project-form",
57+
htmlType: "submit",
58+
}}
59+
>
60+
{error && (
61+
<Alert type="error" message={error.response.errors[0].message} />
62+
)}
63+
<p>
64+
Choose a new name for the{" "}
65+
<Typography.Text style={{ fontWeight: 800 }}>
66+
{projectToRename?.name}
67+
</Typography.Text>{" "}
68+
project.
69+
</p>
70+
71+
<Form
72+
style={{ marginTop: token.marginLG }}
73+
form={form}
74+
name="rename-project-form"
75+
onFinish={handleRename}
76+
initialValues={{
77+
name: projectToRename?.name,
78+
}}
79+
>
80+
<Space direction="vertical" style={{ width: "100%" }} size="middle">
81+
{error && (
82+
<Alert
83+
type="error"
84+
description={
85+
error?.response.errors?.[0].message ?? "Something went wrong"
86+
}
87+
/>
88+
)}
89+
90+
<Form.Item
91+
name="name"
92+
rules={[
93+
{
94+
required: true,
95+
message: "Name must be provided",
96+
},
97+
]}
98+
>
99+
<Input placeholder="Project name" />
100+
</Form.Item>
101+
</Space>
102+
</Form>
103+
</Modal>
104+
);
105+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { graphql } from "../../../../@generated/graphql";
2+
3+
export const CREATE_PROJECT = graphql(/* GraphQL */ `
4+
mutation createProject($data: CreateProjectInput!) {
5+
createProject(data: $data) {
6+
id
7+
organizationId
8+
name
9+
}
10+
}
11+
`);
12+
13+
export const DELETE_PROJECT = graphql(/* GraphQL */ `
14+
mutation deleteProject($data: ProjectWhereUniqueInput!) {
15+
deleteProject(data: $data) {
16+
id
17+
}
18+
}
19+
`);
20+
21+
export const UPDATE_PROJECT_SETTINGS = graphql(/* GraphQL */ `
22+
mutation updateProjectSettings($data: UpdateProjectSettingsInput!) {
23+
updateProjectSettings(data: $data) {
24+
id
25+
}
26+
}
27+
`);

0 commit comments

Comments
 (0)