From 08c0eb5ffd659ca94acdb1a58931eda8884c8411 Mon Sep 17 00:00:00 2001
From: LeeJongBeom <52884648+devleejb@users.noreply.github.com>
Date: Mon, 22 Jan 2024 16:54:04 +0900
Subject: [PATCH] (FE) Document Creation within Workspace Document Name (#81)
* Add create note button
* Implement create modal
* Implement create document
---
frontend/package-lock.json | 117 +++++++++++++++++-
frontend/package.json | 5 +-
frontend/src/App.tsx | 5 +
.../components/drawers/WorkspaceDrawer.tsx | 33 +++++
.../src/components/modals/CreateModal.tsx | 60 +++++++++
frontend/src/hooks/api/types/document.d.ts | 5 -
frontend/src/hooks/api/types/workspace.d.ts | 6 +
.../hooks/api/types/workspaceDocument.d.ts | 12 ++
frontend/src/hooks/api/workspace.ts | 19 ++-
frontend/src/hooks/api/workspaceDocument.ts | 28 ++++-
frontend/src/pages/workspace/Index.tsx | 2 +-
11 files changed, 276 insertions(+), 16 deletions(-)
create mode 100644 frontend/src/components/modals/CreateModal.tsx
create mode 100644 frontend/src/hooks/api/types/workspaceDocument.d.ts
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f1153a75..fca8a7fe 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -14,8 +14,9 @@
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.8",
- "@mui/icons-material": "^5.15.4",
+ "@mui/icons-material": "^5.15.5",
"@mui/material": "^5.15.3",
+ "@mui/x-date-pickers": "^6.19.0",
"@react-hook/window-size": "^3.1.1",
"@reduxjs/toolkit": "^2.0.1",
"@swc/helpers": "^0.5.3",
@@ -34,6 +35,8 @@
"randomcolor": "^0.6.2",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
+ "react-hook-form": "^7.49.3",
+ "react-hook-form-mui": "^7.0.0-beta.0",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^9.0.4",
"react-resizable-layout": "^0.7.2",
@@ -1410,11 +1413,11 @@
}
},
"node_modules/@mui/icons-material": {
- "version": "5.15.4",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.4.tgz",
- "integrity": "sha512-q/Yk7aokN8qGMpR7bwoDpBSeaNe6Bv7vaY9yHYodP37c64TM6ime05ueb/wgksOVszrKkNXC67E/XYbRWOoUFA==",
+ "version": "5.15.5",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.5.tgz",
+ "integrity": "sha512-qiql0fd1JY7TZ1wm1RldvU7sL8QUatE9OC12i/qm5rnm/caTFyAfOyTIR7qqxorsJvoZGyrzwoMkal6Ij9kM0A==",
"dependencies": {
- "@babel/runtime": "^7.23.7"
+ "@babel/runtime": "^7.23.8"
},
"engines": {
"node": ">=12.0.0"
@@ -1614,6 +1617,71 @@
}
}
},
+ "node_modules/@mui/x-date-pickers": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.19.0.tgz",
+ "integrity": "sha512-/GccT+iFJTKjI6b9b0MWojyRKnizL/VYYAfPnR1q0wSVVXjYv7a1NK0uQlan4JbnovqoQCNVeTOCy/0bUJyD2Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "@mui/base": "^5.0.0-beta.22",
+ "@mui/utils": "^5.14.16",
+ "@types/react-transition-group": "^4.4.8",
+ "clsx": "^2.0.0",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.8.6",
+ "@mui/system": "^5.8.0",
+ "date-fns": "^2.25.0 || ^3.2.0",
+ "date-fns-jalali": "^2.13.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -5582,6 +5650,45 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.49.3",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz",
+ "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==",
+ "engines": {
+ "node": ">=18",
+ "pnpm": "8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-hook-form-mui": {
+ "version": "7.0.0-beta.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form-mui/-/react-hook-form-mui-7.0.0-beta.0.tgz",
+ "integrity": "sha512-pj+GNsrWtmhS1BhXJdEqSxPcsuZqI2xJ/N/yPMVKT0iVwKPrIzl9MkC564Xtgti3Ucd4fByEmVkUacS4SWHxSA==",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@mui/icons-material": ">= 5.x <6",
+ "@mui/material": ">= 5.x <6",
+ "@mui/x-date-pickers": ">=6.1.0 <7",
+ "react": ">=17 <19",
+ "react-hook-form": ">=7.33.1"
+ },
+ "peerDependenciesMeta": {
+ "@mui/icons-material": {
+ "optional": true
+ },
+ "@mui/x-date-pickers": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-infinite-scroller": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 91e03aac..2607685b 100755
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,8 +18,9 @@
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.8",
- "@mui/icons-material": "^5.15.4",
+ "@mui/icons-material": "^5.15.5",
"@mui/material": "^5.15.3",
+ "@mui/x-date-pickers": "^6.19.0",
"@react-hook/window-size": "^3.1.1",
"@reduxjs/toolkit": "^2.0.1",
"@swc/helpers": "^0.5.3",
@@ -38,6 +39,8 @@
"randomcolor": "^0.6.2",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
+ "react-hook-form": "^7.49.3",
+ "react-hook-form-mui": "^7.0.0-beta.0",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^9.0.4",
"react-resizable-layout": "^0.7.2",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1ea930ba..feb453f5 100755
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -22,6 +22,11 @@ function App() {
const defaultMode = prefersDarkMode ? "dark" : "light";
return createTheme({
+ typography: {
+ button: {
+ textTransform: "none",
+ },
+ },
palette: {
mode: config.theme == "default" ? defaultMode : config.theme,
},
diff --git a/frontend/src/components/drawers/WorkspaceDrawer.tsx b/frontend/src/components/drawers/WorkspaceDrawer.tsx
index f99002e9..d97576ef 100644
--- a/frontend/src/components/drawers/WorkspaceDrawer.tsx
+++ b/frontend/src/components/drawers/WorkspaceDrawer.tsx
@@ -1,6 +1,7 @@
import {
Avatar,
Box,
+ Button,
Divider,
Drawer,
IconButton,
@@ -20,6 +21,9 @@ import { useGetWorkspaceQuery } from "../../hooks/api/workspace";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import WorkspaceListPopover from "../popovers/WorkspaceListPopover";
+import AddIcon from "@mui/icons-material/Add";
+import CreateModal from "../modals/CreateModal";
+import { useCreateDocumentMutation } from "../../hooks/api/workspaceDocument";
const DRAWER_WIDTH = 240;
@@ -27,10 +31,12 @@ function WorkspaceDrawer() {
const params = useParams();
const userStore = useSelector(selectUser);
const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug);
+ const { mutateAsync: createDocument } = useCreateDocumentMutation(workspace?.id || "");
const [profileAnchorEl, setProfileAnchorEl] = useState<(EventTarget & Element) | null>(null);
const [workspaceListAnchorEl, setWorkspaceListAnchorEl] = useState<
(EventTarget & Element) | null
>(null);
+ const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
const handleOpenProfilePopover: MouseEventHandler = (event) => {
setProfileAnchorEl(event.currentTarget);
@@ -48,6 +54,14 @@ function WorkspaceDrawer() {
setWorkspaceListAnchorEl(null);
};
+ const handleCreateWorkspace = async (data: { title: string }) => {
+ await createDocument(data);
+ };
+
+ const handleCreateWorkspaceModalOpen = () => {
+ setCreateWorkspaceModalOpen((prev) => !prev);
+ };
+
return (
+
+ }
+ sx={{
+ width: 1,
+ }}
+ onClick={handleCreateWorkspaceModalOpen}
+ >
+ New Note
+
+
+
@@ -108,6 +135,12 @@ function WorkspaceDrawer() {
/>
+
);
}
diff --git a/frontend/src/components/modals/CreateModal.tsx b/frontend/src/components/modals/CreateModal.tsx
new file mode 100644
index 00000000..42b12a74
--- /dev/null
+++ b/frontend/src/components/modals/CreateModal.tsx
@@ -0,0 +1,60 @@
+import { Button, FormControl, Modal, ModalProps, Paper, Stack, Typography } from "@mui/material";
+import { FormContainer, TextFieldElement } from "react-hook-form-mui";
+
+interface CreateRequest {
+ title: string;
+}
+
+interface CreateModalProps extends Omit {
+ title: string;
+ onSuccess: (data: CreateRequest) => Promise;
+}
+
+function CreateModal(props: CreateModalProps) {
+ const { title, onSuccess, ...modalProps } = props;
+
+ const handleCreate = async (data: CreateRequest) => {
+ await onSuccess(data);
+ modalProps?.onClose?.(new Event("Close Modal"), "escapeKeyDown");
+ };
+
+ return (
+
+
+
+ Create New {title}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default CreateModal;
diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts
index 5ae36e45..7bc536e1 100644
--- a/frontend/src/hooks/api/types/document.d.ts
+++ b/frontend/src/hooks/api/types/document.d.ts
@@ -7,8 +7,3 @@ export class Document {
createdAt: Date;
updatedAt: Date;
}
-
-export class GetWorkspaceDocumentListResponse {
- cursor: string | null;
- documents: Array;
-}
diff --git a/frontend/src/hooks/api/types/workspace.d.ts b/frontend/src/hooks/api/types/workspace.d.ts
index bc7af04b..19497919 100644
--- a/frontend/src/hooks/api/types/workspace.d.ts
+++ b/frontend/src/hooks/api/types/workspace.d.ts
@@ -12,3 +12,9 @@ export class GetWorkspaceListResponse {
cursor: string | null;
workspaces: Array;
}
+
+export class CreateWorkspaceRequest {
+ title: string;
+}
+
+export class CreateWorkspaceResponse extends Workspace {}
diff --git a/frontend/src/hooks/api/types/workspaceDocument.d.ts b/frontend/src/hooks/api/types/workspaceDocument.d.ts
new file mode 100644
index 00000000..ee35a854
--- /dev/null
+++ b/frontend/src/hooks/api/types/workspaceDocument.d.ts
@@ -0,0 +1,12 @@
+import { Document } from "./document";
+
+export class GetWorkspaceDocumentListResponse {
+ cursor: string | null;
+ documents: Array;
+}
+
+export class CreateDocumentRequest {
+ title: string;
+}
+
+export class CreateDocumentResponse extends Document {}
diff --git a/frontend/src/hooks/api/workspace.ts b/frontend/src/hooks/api/workspace.ts
index f5264303..b34ebe78 100644
--- a/frontend/src/hooks/api/workspace.ts
+++ b/frontend/src/hooks/api/workspace.ts
@@ -1,6 +1,11 @@
-import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
+import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import axios from "axios";
-import { GetWorkspaceListResponse, GetWorkspaceResponse } from "./types/workspace";
+import {
+ CreateWorkspaceRequest,
+ CreateWorkspaceResponse,
+ GetWorkspaceListResponse,
+ GetWorkspaceResponse,
+} from "./types/workspace";
export const generateGetWorkspaceQueryKey = (workspaceId: string) => {
return ["workspaces", workspaceId];
@@ -44,3 +49,13 @@ export const useGetWorkspaceListQuery = () => {
return query;
};
+
+export const useCreateWorkspaceMutation = () => {
+ return useMutation({
+ mutationFn: async (data: CreateWorkspaceRequest) => {
+ const res = await axios.post("/workspaces", data);
+
+ return res.data;
+ },
+ });
+};
diff --git a/frontend/src/hooks/api/workspaceDocument.ts b/frontend/src/hooks/api/workspaceDocument.ts
index f97d533d..1e637b88 100644
--- a/frontend/src/hooks/api/workspaceDocument.ts
+++ b/frontend/src/hooks/api/workspaceDocument.ts
@@ -1,6 +1,10 @@
-import { useInfiniteQuery } from "@tanstack/react-query";
+import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
-import { GetWorkspaceDocumentListResponse } from "./types/document";
+import {
+ CreateDocumentRequest,
+ CreateDocumentResponse,
+ GetWorkspaceDocumentListResponse,
+} from "./types/workspaceDocument";
export const generateGetWorkspaceDocumentListQueryKey = (workspaceId: string) => {
return ["workspaces", workspaceId, "documents"];
@@ -28,3 +32,23 @@ export const useGetWorkspaceDocumentListQuery = (workspaceId?: string) => {
return query;
};
+
+export const useCreateDocumentMutation = (workspaceId: string) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (data: CreateDocumentRequest) => {
+ const res = await axios.post(
+ `/workspaces/${workspaceId}/documents`,
+ data
+ );
+
+ return res.data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: generateGetWorkspaceDocumentListQueryKey(workspaceId),
+ });
+ },
+ });
+};
diff --git a/frontend/src/pages/workspace/Index.tsx b/frontend/src/pages/workspace/Index.tsx
index 6cbac467..e42a056a 100644
--- a/frontend/src/pages/workspace/Index.tsx
+++ b/frontend/src/pages/workspace/Index.tsx
@@ -22,7 +22,7 @@ function WorkspaceIndex() {
return (
-
+