From 4b9dd237d3c87ab1290001f50f86163c89e19804 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:01:36 +0900 Subject: [PATCH 01/37] Change login page url --- frontend/src/pages/{ => login}/Index.tsx | 6 +++--- frontend/src/routes.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) rename frontend/src/pages/{ => login}/Index.tsx (93%) diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/login/Index.tsx similarity index 93% rename from frontend/src/pages/Index.tsx rename to frontend/src/pages/login/Index.tsx index d6f257a7..db4c0cda 100644 --- a/frontend/src/pages/Index.tsx +++ b/frontend/src/pages/login/Index.tsx @@ -1,5 +1,5 @@ import { Box, Container, Divider, Grid, Paper, Stack, Typography } from "@mui/material"; -import CodePairIcon from "../components/icons/CodePairIcon"; +import CodePairIcon from "../../components/icons/CodePairIcon"; import { GithubLoginButton } from "react-social-login-buttons"; const socialLoginList = [ @@ -9,7 +9,7 @@ const socialLoginList = [ }, ]; -function Index() { +function LoginIndex() { const handleLogin = (provider: string) => { window.location.href = `${import.meta.env.VITE_API_ADDR}/auth/login/${provider}`; }; @@ -57,4 +57,4 @@ function Index() { ); } -export default Index; +export default LoginIndex; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 310f5c99..449a471f 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -1,7 +1,7 @@ import EditorLayout from "./components/layouts/EditorLayout"; import EditorIndex from "./pages/document/Index"; import MainLayout from "./components/layouts/MainLayout"; -import Index from "./pages/Index"; +import LoginIndex from "./pages/login/Index"; import CallbackIndex from "./pages/auth/callback/Index"; import WorkspaceLayout from "./components/layouts/WorkspaceLayout"; import GuestRoute from "./components/common/GuestRoute"; @@ -34,8 +34,8 @@ const codePairRoutes: Array = [ element: , children: [ { - path: "", - element: , + path: "login", + element: , }, ], }, From 567740fd64d4efbdef2b12ad08f9f587c40515e5 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:02:43 +0900 Subject: [PATCH 02/37] Add new index page --- frontend/src/pages/Index.tsx | 7 +++++++ frontend/src/routes.tsx | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 frontend/src/pages/Index.tsx diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx new file mode 100644 index 00000000..a46cf09b --- /dev/null +++ b/frontend/src/pages/Index.tsx @@ -0,0 +1,7 @@ +import { Box } from "@mui/material"; + +function Index() { + return ; +} + +export default Index; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 449a471f..e13599b4 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -9,6 +9,7 @@ import PrivateRoute from "./components/common/PrivateRoute"; import WorkspaceIndex from "./pages/workspace/Index"; import CodePairError from "./components/common/CodePairError"; import JoinIndex from "./pages/workspace/join/Index"; +import Index from "./pages/Index"; interface CodePairRoute { path: string; @@ -33,6 +34,10 @@ const codePairRoutes: Array = [ accessType: AccessType.GUEST, element: , children: [ + { + path: "", + element: , + }, { path: "login", element: , From d5041378397cd68c7601fefa7d856bb4d0125c81 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:05:22 +0900 Subject: [PATCH 03/37] Add login button to main header --- frontend/src/components/headers/MainHeader.tsx | 16 ++++++++++++++-- frontend/src/components/layouts/MainLayout.tsx | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/headers/MainHeader.tsx b/frontend/src/components/headers/MainHeader.tsx index 091c2e26..3e864568 100644 --- a/frontend/src/components/headers/MainHeader.tsx +++ b/frontend/src/components/headers/MainHeader.tsx @@ -1,8 +1,15 @@ -import { AppBar, Stack, Toolbar } from "@mui/material"; +import { AppBar, Button, Stack, Toolbar } from "@mui/material"; import ThemeButton from "../common/ThemeButton"; import CodePairIcon from "../icons/CodePairIcon"; +import { useNavigate } from "react-router-dom"; function MainHeader() { + const navigate = useNavigate(); + + const handleMoveToLogin = () => { + navigate("/login"); + }; + return ( @@ -13,7 +20,12 @@ function MainHeader() { alignItems="center" > - + + + + diff --git a/frontend/src/components/layouts/MainLayout.tsx b/frontend/src/components/layouts/MainLayout.tsx index 41af4695..22420808 100644 --- a/frontend/src/components/layouts/MainLayout.tsx +++ b/frontend/src/components/layouts/MainLayout.tsx @@ -1,9 +1,11 @@ import { Stack } from "@mui/material"; import { Outlet } from "react-router-dom"; +import MainHeader from "../headers/MainHeader"; function MainLayout() { return ( + ); From 5fdb840676404f5a2459ba6488a3469e00132a0d Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:08:45 +0900 Subject: [PATCH 04/37] Change height of main layout --- frontend/src/components/layouts/MainLayout.tsx | 2 +- frontend/src/pages/login/Index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/layouts/MainLayout.tsx b/frontend/src/components/layouts/MainLayout.tsx index 22420808..d147f6b6 100644 --- a/frontend/src/components/layouts/MainLayout.tsx +++ b/frontend/src/components/layouts/MainLayout.tsx @@ -4,7 +4,7 @@ import MainHeader from "../headers/MainHeader"; function MainLayout() { return ( - + diff --git a/frontend/src/pages/login/Index.tsx b/frontend/src/pages/login/Index.tsx index db4c0cda..b7d4eed9 100644 --- a/frontend/src/pages/login/Index.tsx +++ b/frontend/src/pages/login/Index.tsx @@ -15,8 +15,8 @@ function LoginIndex() { }; return ( - - + + From b6d274e44e787e724c818d5dd881800466572744 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:10:04 +0900 Subject: [PATCH 05/37] Change workspace url spec --- frontend/src/routes.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index e13599b4..7e045c15 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -45,12 +45,12 @@ const codePairRoutes: Array = [ ], }, { - path: "workspace", + path: ":workspaceSlug", accessType: AccessType.PRIVATE, element: , children: [ { - path: ":workspaceSlug", + path: "", element: , }, ], From 68f1ee116b7dd1da12212106b2d6e5eb97db41bb Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:12:24 +0900 Subject: [PATCH 06/37] Change redirect url after login --- frontend/src/components/common/GuestRoute.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/GuestRoute.tsx b/frontend/src/components/common/GuestRoute.tsx index c83d70f6..f3504a36 100644 --- a/frontend/src/components/common/GuestRoute.tsx +++ b/frontend/src/components/common/GuestRoute.tsx @@ -17,7 +17,7 @@ const GuestRoute = (props: RejectLoggedInRouteProps) => { if (isLoggedIn) { return ( From d59de65bf17a32d7fb62edd7dc3d0415a940e37a Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:12:57 +0900 Subject: [PATCH 07/37] Change url of workspace --- frontend/src/components/popovers/WorkspaceListPopover.tsx | 2 +- frontend/src/pages/workspace/join/Index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/popovers/WorkspaceListPopover.tsx b/frontend/src/components/popovers/WorkspaceListPopover.tsx index 631ca23b..d25b9839 100644 --- a/frontend/src/components/popovers/WorkspaceListPopover.tsx +++ b/frontend/src/components/popovers/WorkspaceListPopover.tsx @@ -39,7 +39,7 @@ function WorkspaceListPopover(props: WorkspaceListPopoverProps) { const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false); const moveToWorkspace = (slug: string) => { - navigate(`/workspace/${slug}`); + navigate(`/${slug}`); }; const handleMoveToSelectedWorkspace = (workspaceSlug: string) => { diff --git a/frontend/src/pages/workspace/join/Index.tsx b/frontend/src/pages/workspace/join/Index.tsx index bc43af69..6ced7840 100644 --- a/frontend/src/pages/workspace/join/Index.tsx +++ b/frontend/src/pages/workspace/join/Index.tsx @@ -12,7 +12,7 @@ function JoinIndex() { if (!params.invitationToken) return; joinWorkspace({ invitationToken: params.invitationToken }).then((data) => { - navigate(`/workspace/${data.slug}`); + navigate(`/${data.slug}`); }); }, [joinWorkspace, navigate, params.invitationToken]); From b8f0856c59b859a5585dcd3f444648e7d3d9ab10 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:16:38 +0900 Subject: [PATCH 08/37] Change component name from `Editor` to `Document` --- .../headers/{EditorHeader.tsx => DocumentHeader.tsx} | 4 ++-- .../layouts/{EditorLayout.tsx => DocumentLayout.tsx} | 8 ++++---- frontend/src/pages/document/Index.tsx | 4 ++-- frontend/src/routes.tsx | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) rename frontend/src/components/headers/{EditorHeader.tsx => DocumentHeader.tsx} (98%) rename frontend/src/components/layouts/{EditorLayout.tsx => DocumentLayout.tsx} (54%) diff --git a/frontend/src/components/headers/EditorHeader.tsx b/frontend/src/components/headers/DocumentHeader.tsx similarity index 98% rename from frontend/src/components/headers/EditorHeader.tsx rename to frontend/src/components/headers/DocumentHeader.tsx index 7827ade9..918418b2 100644 --- a/frontend/src/components/headers/EditorHeader.tsx +++ b/frontend/src/components/headers/DocumentHeader.tsx @@ -21,7 +21,7 @@ import { useList } from "react-use"; import { ActorID } from "yorkie-js-sdk"; import { YorkieCodeMirrorPresenceType } from "../../utils/yorkie/yorkieSync"; -function EditorHeader() { +function DocumentHeader() { const dispatch = useDispatch(); const editorState = useSelector(selectEditor); const [ @@ -129,4 +129,4 @@ function EditorHeader() { ); } -export default EditorHeader; +export default DocumentHeader; diff --git a/frontend/src/components/layouts/EditorLayout.tsx b/frontend/src/components/layouts/DocumentLayout.tsx similarity index 54% rename from frontend/src/components/layouts/EditorLayout.tsx rename to frontend/src/components/layouts/DocumentLayout.tsx index c1346bd8..1eb514b0 100644 --- a/frontend/src/components/layouts/EditorLayout.tsx +++ b/frontend/src/components/layouts/DocumentLayout.tsx @@ -1,14 +1,14 @@ import { Box } from "@mui/material"; import { Outlet } from "react-router-dom"; -import EditorHeader from "../headers/EditorHeader"; +import DocumentHeader from "../headers/DocumentHeader"; -function EditorLayout() { +function DocumentLayout() { return ( - + ); } -export default EditorLayout; +export default DocumentLayout; diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx index 7d7d11bc..3ed15922 100644 --- a/frontend/src/pages/document/Index.tsx +++ b/frontend/src/pages/document/Index.tsx @@ -18,7 +18,7 @@ import { useGetDocumentBySharingTokenQuery, useGetDocumentQuery } from "../../ho import { AuthContext } from "../../contexts/AuthContext"; import { selectUser } from "../../store/userSlice"; -function EditorIndex() { +function DocumentIndex() { const dispatch = useDispatch(); const params = useParams(); const userStore = useSelector(selectUser); @@ -142,4 +142,4 @@ function EditorIndex() { ); } -export default EditorIndex; +export default DocumentIndex; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 7e045c15..e0cfc718 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -1,5 +1,4 @@ -import EditorLayout from "./components/layouts/EditorLayout"; -import EditorIndex from "./pages/document/Index"; +import DocumentIndex from "./pages/document/Index"; import MainLayout from "./components/layouts/MainLayout"; import LoginIndex from "./pages/login/Index"; import CallbackIndex from "./pages/auth/callback/Index"; @@ -10,6 +9,7 @@ import WorkspaceIndex from "./pages/workspace/Index"; import CodePairError from "./components/common/CodePairError"; import JoinIndex from "./pages/workspace/join/Index"; import Index from "./pages/Index"; +import DocumentLayout from "./components/layouts/DocumentLayout"; interface CodePairRoute { path: string; @@ -58,11 +58,11 @@ const codePairRoutes: Array = [ { path: "document", accessType: AccessType.PUBLIC, - element: , + element: , children: [ { path: ":documentSlug", - element: , + element: , }, ], }, From 275e55092928a011d4a7e153ccb6e9dd9848ec02 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:22:57 +0900 Subject: [PATCH 09/37] Change `injectProtectedRoute` implementation to support protecting children --- frontend/src/routes.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index e0cfc718..efb23699 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -13,12 +13,13 @@ import DocumentLayout from "./components/layouts/DocumentLayout"; interface CodePairRoute { path: string; - accessType: AccessType; + accessType?: AccessType; element: JSX.Element; errorElement?: JSX.Element; children?: { path: string; element: JSX.Element; + accessType?: AccessType; }[]; } @@ -56,12 +57,12 @@ const codePairRoutes: Array = [ ], }, { - path: "document", - accessType: AccessType.PUBLIC, + path: ":workspaceSlug", element: , children: [ { path: ":documentSlug", + accessType: AccessType.PRIVATE, element: , }, ], @@ -78,14 +79,24 @@ const codePairRoutes: Array = [ }, ]; -const injectProtectedRoute = (routes: typeof codePairRoutes) => { - return routes.map((route) => { +const injectProtectedRoute = (routes: Array) => { + const injectProtectedComp = (route: CodePairRoute) => { if (route.accessType === AccessType.PRIVATE) { route.element = {route.element}; } else if (route.accessType === AccessType.GUEST) { route.element = {route.element}; } + return route; + }; + + return routes.map((route) => { + route = injectProtectedComp(route); + + if (route?.children) { + route.children = route.children.map((route) => injectProtectedComp(route)); + } + route.errorElement = ; return route; From 09c348cedd86a9faca09bde9a9052565963c36ca Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:25:16 +0900 Subject: [PATCH 10/37] Change documentSlug to documentId in URL --- frontend/src/routes.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index efb23699..1d5c2fcd 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -24,8 +24,8 @@ interface CodePairRoute { } const enum AccessType { + PUBLIC, // Everyone can access (Default) PRIVATE, // Authroized user can access only - PUBLIC, // Everyone can access GUEST, // Not authorized user can access only } @@ -61,7 +61,7 @@ const codePairRoutes: Array = [ element: , children: [ { - path: ":documentSlug", + path: ":documentId", accessType: AccessType.PRIVATE, element: , }, From 2bc2378ec2c4decad317ce718ea5bafc292ae781 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:28:47 +0900 Subject: [PATCH 11/37] Delete share mode in `DocumentIndex` --- frontend/src/pages/document/Index.tsx | 45 ++++++--------------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx index 3ed15922..997d2138 100644 --- a/frontend/src/pages/document/Index.tsx +++ b/frontend/src/pages/document/Index.tsx @@ -1,7 +1,7 @@ -import { useContext, useEffect } from "react"; +import { useEffect } from "react"; import Editor from "../../components/editor/Editor"; import * as yorkie from "yorkie-js-sdk"; -import { selectEditor, setClient, setDoc, setShareRole } from "../../store/editorSlice"; +import { selectEditor, setClient, setDoc } from "../../store/editorSlice"; import { useDispatch, useSelector } from "react-redux"; import { YorkieCodeMirrorDocType, @@ -13,32 +13,24 @@ import { Box, Paper } from "@mui/material"; import Resizable from "react-resizable-layout"; import { useWindowWidth } from "@react-hook/window-size"; import Preview from "../../components/editor/Preview"; -import { Navigate, useParams, useSearchParams } from "react-router-dom"; -import { useGetDocumentBySharingTokenQuery, useGetDocumentQuery } from "../../hooks/api/document"; -import { AuthContext } from "../../contexts/AuthContext"; +import { useParams } from "react-router-dom"; +import { useGetDocumentQuery } from "../../hooks/api/document"; import { selectUser } from "../../store/userSlice"; function DocumentIndex() { const dispatch = useDispatch(); const params = useParams(); const userStore = useSelector(selectUser); - const { isLoggedIn } = useContext(AuthContext); - const [searchParams] = useSearchParams(); const windowWidth = useWindowWidth(); const editorStore = useSelector(selectEditor); - const { data: document, isError: isDocumentError } = useGetDocumentQuery( - isLoggedIn ? params.documentSlug : null - ); - const { data: sharedDocument, isError: isSharedDocumentError } = - useGetDocumentBySharingTokenQuery(searchParams.get("token")); + const { data: document } = useGetDocumentQuery(params.documentId); useEffect(() => { let client: yorkie.Client; let doc: yorkie.Document; - const yorkieDocuentId = document?.yorkieDocumentId || sharedDocument?.yorkieDocumentId; - const name = searchParams.get("token") ? "Anonymous" : userStore.data?.nickname; + const yorkieDocuentId = document?.yorkieDocumentId; - if (!yorkieDocuentId || !name) return; + if (!yorkieDocuentId) return; const initializeYorkie = async () => { client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, { @@ -50,7 +42,7 @@ function DocumentIndex() { await client.attach(doc, { initialPresence: { - name, + name: userStore.data?.nickname as string, color: Color(randomColor()).fade(0.15).toString(), selection: null, }, @@ -70,26 +62,7 @@ function DocumentIndex() { cleanUp(); }; - }, [ - dispatch, - document?.yorkieDocumentId, - sharedDocument?.yorkieDocumentId, - userStore.data?.nickname, - searchParams, - ]); - - useEffect(() => { - if (!sharedDocument) return; - - dispatch(setShareRole(sharedDocument.role)); - - return () => { - setShareRole(null); - }; - }, [dispatch, sharedDocument, sharedDocument?.role]); - - if (isDocumentError || isSharedDocumentError) - return ; + }, [dispatch, document?.yorkieDocumentId, userStore.data?.nickname]); return ( From 280bfbce2a6ff55dbb2a676f423e947bd4ccad40 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:33:22 +0900 Subject: [PATCH 12/37] Change API path for document --- backend/src/documents/documents.controller.ts | 20 ------------------ backend/src/documents/documents.service.ts | 21 ------------------- .../types/find-document-response.type.ts | 3 --- .../find-workspace-document-response.type.ts | 3 +++ .../workspace-documents.controller.ts | 20 ++++++++++++++++++ .../workspace-documents.service.ts | 19 +++++++++++++++++ 6 files changed, 42 insertions(+), 44 deletions(-) delete mode 100644 backend/src/documents/types/find-document-response.type.ts create mode 100644 backend/src/workspace-documents/types/find-workspace-document-response.type.ts diff --git a/backend/src/documents/documents.controller.ts b/backend/src/documents/documents.controller.ts index 4498f9ca..b2cf380f 100644 --- a/backend/src/documents/documents.controller.ts +++ b/backend/src/documents/documents.controller.ts @@ -12,8 +12,6 @@ import { } from "@nestjs/swagger"; import { FindDocumentFromSharingTokenResponse } from "./types/find-document-from-sharing-token-response.type"; import { HttpExceptionResponse } from "src/utils/types/http-exception-response.type"; -import { FindDocumentResponse } from "./types/find-document-response.type"; -import { AuthroizedRequest } from "src/utils/types/req.type"; @ApiTags("Documents") @Controller("documents") @@ -41,22 +39,4 @@ export class DocumentsController { ): Promise { return this.documentsService.findDocumentFromSharingToken(token); } - - @Get(":document_slug") - @ApiOperation({ - summary: "Retrieve a Document in the Workspace", - description: "If the user has the access permissions, return a document.", - }) - @ApiFoundResponse({ type: FindDocumentResponse }) - @ApiNotFoundResponse({ - type: HttpExceptionResponse, - description: - "The workspace or document does not exist, or the user lacks the appropriate permissions.", - }) - async findOne( - @Req() req: AuthroizedRequest, - @Param("document_slug") documentSlug: string - ): Promise { - return this.documentsService.findOneBySlug(req.user.id, documentSlug); - } } diff --git a/backend/src/documents/documents.service.ts b/backend/src/documents/documents.service.ts index d46ff037..4d4fee61 100644 --- a/backend/src/documents/documents.service.ts +++ b/backend/src/documents/documents.service.ts @@ -43,25 +43,4 @@ export class DocumentsService { role, }; } - - async findOneBySlug(userId: string, documentSlug: string) { - try { - const document = await this.prismaService.document.findFirstOrThrow({ - where: { - slug: documentSlug, - }, - }); - - await this.prismaService.userWorkspace.findFirstOrThrow({ - where: { - userId, - workspaceId: document.workspaceId, - }, - }); - - return document; - } catch (e) { - throw new NotFoundException(); - } - } } diff --git a/backend/src/documents/types/find-document-response.type.ts b/backend/src/documents/types/find-document-response.type.ts deleted file mode 100644 index ddd4291b..00000000 --- a/backend/src/documents/types/find-document-response.type.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { WorkspaceDocumentDomain } from "../../workspace-documents/types/workspace-document-domain.type"; - -export class FindDocumentResponse extends WorkspaceDocumentDomain {} diff --git a/backend/src/workspace-documents/types/find-workspace-document-response.type.ts b/backend/src/workspace-documents/types/find-workspace-document-response.type.ts new file mode 100644 index 00000000..4b13d4bf --- /dev/null +++ b/backend/src/workspace-documents/types/find-workspace-document-response.type.ts @@ -0,0 +1,3 @@ +import { WorkspaceDocumentDomain } from "./workspace-document-domain.type"; + +export class FindWorkspaceDocumentResponse extends WorkspaceDocumentDomain {} diff --git a/backend/src/workspace-documents/workspace-documents.controller.ts b/backend/src/workspace-documents/workspace-documents.controller.ts index c33608e0..9ab4fd1f 100644 --- a/backend/src/workspace-documents/workspace-documents.controller.ts +++ b/backend/src/workspace-documents/workspace-documents.controller.ts @@ -28,6 +28,7 @@ import { HttpExceptionResponse } from "src/utils/types/http-exception-response.t import { FindWorkspaceDocumentsResponse } from "./types/find-workspace-documents-response.type"; import { CreateWorkspaceDocumentShareTokenResponse } from "./types/create-workspace-document-share-token-response.type"; import { CreateWorkspaceDocumentShareTokenDto } from "./dto/create-workspace-document-share-token.dto"; +import { FindWorkspaceDocumentResponse } from "./types/find-workspace-document-response.type"; @ApiTags("Workspace.Documents") @ApiBearerAuth() @@ -67,6 +68,25 @@ export class WorkspaceDocumentsController { return this.workspaceDocumentsService.findMany(req.user.id, workspaceId, pageSize, cursor); } + @Get(":document_id") + @ApiOperation({ + summary: "Retrieve a Document in the Workspace", + description: "If the user has the access permissions, return a document.", + }) + @ApiFoundResponse({ type: FindWorkspaceDocumentResponse }) + @ApiNotFoundResponse({ + type: HttpExceptionResponse, + description: + "The workspace or document does not exist, or the user lacks the appropriate permissions.", + }) + async findOne( + @Req() req: AuthroizedRequest, + @Param("workspace_id") workspaceId: string, + @Param("document_id") documentId: string + ): Promise { + return this.workspaceDocumentsService.findOne(req.user.id, workspaceId, documentId); + } + @Post() @ApiOperation({ summary: "Create a Document in a Workspace", diff --git a/backend/src/workspace-documents/workspace-documents.service.ts b/backend/src/workspace-documents/workspace-documents.service.ts index 5e125db6..111bc70e 100644 --- a/backend/src/workspace-documents/workspace-documents.service.ts +++ b/backend/src/workspace-documents/workspace-documents.service.ts @@ -87,6 +87,25 @@ export class WorkspaceDocumentsService { }; } + async findOne(userId: string, workspaceId: string, documentId: string) { + try { + await this.prismaService.userWorkspace.findFirstOrThrow({ + where: { + userId, + workspaceId, + }, + }); + + return this.prismaService.document.findUniqueOrThrow({ + where: { + id: documentId, + }, + }); + } catch (e) { + throw new NotFoundException(); + } + } + async createSharingToken( userId: string, workspaceId: string, From e9c7b6623b4eda416c24ffb588bcbb2d0520fdcc Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 16:40:36 +0900 Subject: [PATCH 13/37] Change document page url --- .../src/components/cards/DocumentCard.tsx | 5 ++-- frontend/src/components/modals/ShareModal.tsx | 6 +++-- frontend/src/hooks/api/document.ts | 24 ++---------------- frontend/src/hooks/api/types/document.d.ts | 2 -- .../hooks/api/types/workspaceDocument.d.ts | 2 ++ frontend/src/hooks/api/workspaceDocument.ts | 25 ++++++++++++++++++- frontend/src/pages/document/Index.tsx | 6 +++-- 7 files changed, 39 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/cards/DocumentCard.tsx b/frontend/src/components/cards/DocumentCard.tsx index 4416ebeb..f857b884 100644 --- a/frontend/src/components/cards/DocumentCard.tsx +++ b/frontend/src/components/cards/DocumentCard.tsx @@ -2,7 +2,7 @@ import moment from "moment"; import { Card, CardActionArea, CardContent, Stack, Typography } from "@mui/material"; import AccessTimeIcon from "@mui/icons-material/AccessTime"; import { Document } from "../../hooks/api/types/document.d"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; interface DocumentCardProps { document: Document; @@ -11,9 +11,10 @@ interface DocumentCardProps { function DocumentCard(props: DocumentCardProps) { const { document } = props; const navigate = useNavigate(); + const params = useParams(); const handleToDocument = () => { - navigate(`/document/${document.slug}`); + navigate(`/${params.workspaceSlug}/${document.id}`); }; return ( diff --git a/frontend/src/components/modals/ShareModal.tsx b/frontend/src/components/modals/ShareModal.tsx index 87afda76..e8d503da 100644 --- a/frontend/src/components/modals/ShareModal.tsx +++ b/frontend/src/components/modals/ShareModal.tsx @@ -14,8 +14,10 @@ import { invitationExpiredStringList } from "../../utils/expire"; import { useState } from "react"; import moment, { unitOfTime } from "moment"; import { useParams } from "react-router"; -import { useGetDocumentQuery } from "../../hooks/api/document"; -import { useCreateWorkspaceSharingTokenMutation } from "../../hooks/api/workspaceDocument"; +import { + useCreateWorkspaceSharingTokenMutation, + useGetDocumentQuery, +} from "../../hooks/api/workspaceDocument"; import { ShareRole } from "../../utils/share"; import clipboard from "clipboardy"; import { useSnackbar } from "notistack"; diff --git a/frontend/src/hooks/api/document.ts b/frontend/src/hooks/api/document.ts index 1a3512d4..a6e88ab7 100644 --- a/frontend/src/hooks/api/document.ts +++ b/frontend/src/hooks/api/document.ts @@ -1,34 +1,14 @@ import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { GetDocumentBySharingTokenResponse, GetDocumentResponse } from "./types/document"; - -export const generateGetDocumentQueryKey = (documentSlug: string) => { - return ["documents", documentSlug]; -}; +import { GetDocumentBySharingTokenResponse } from "./types/document"; export const generateGetDocumentBySharingTokenQueryKey = (sharingToken: string) => { return ["documents", "share", sharingToken]; }; -export const useGetDocumentQuery = (documentSlug?: string | null) => { - const query = useQuery({ - queryKey: generateGetDocumentQueryKey(documentSlug || ""), - enabled: Boolean(documentSlug), - queryFn: async () => { - const res = await axios.get(`/documents/${documentSlug}`); - return res.data; - }, - meta: { - errorMessage: "This is a non-existent or unauthorized document.", - }, - }); - - return query; -}; - export const useGetDocumentBySharingTokenQuery = (sharingToken?: string | null) => { const query = useQuery({ - queryKey: generateGetDocumentQueryKey(sharingToken || ""), + queryKey: generateGetDocumentBySharingTokenQueryKey(sharingToken || ""), enabled: Boolean(sharingToken), queryFn: async () => { const res = await axios.get("/documents/share", { diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts index e7a594ba..aac8faed 100644 --- a/frontend/src/hooks/api/types/document.d.ts +++ b/frontend/src/hooks/api/types/document.d.ts @@ -11,8 +11,6 @@ export class Document { updatedAt: Date; } -export class GetDocumentResponse extends Document {} - export class GetDocumentBySharingTokenResponse extends Document { role: ShareRole; } diff --git a/frontend/src/hooks/api/types/workspaceDocument.d.ts b/frontend/src/hooks/api/types/workspaceDocument.d.ts index 05ae6db4..aea1808e 100644 --- a/frontend/src/hooks/api/types/workspaceDocument.d.ts +++ b/frontend/src/hooks/api/types/workspaceDocument.d.ts @@ -5,6 +5,8 @@ export class GetWorkspaceDocumentListResponse { documents: Array; } +export class GetWorkspaceDocumentResponse extends Document {} + export class CreateDocumentRequest { title: string; } diff --git a/frontend/src/hooks/api/workspaceDocument.ts b/frontend/src/hooks/api/workspaceDocument.ts index ad4da6fb..614617af 100644 --- a/frontend/src/hooks/api/workspaceDocument.ts +++ b/frontend/src/hooks/api/workspaceDocument.ts @@ -1,10 +1,11 @@ -import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; import { CreateDocumentRequest, CreateDocumentResponse, CreateDocumentShareTokenRequest, CreateDocumentShareTokenResponse, + GetWorkspaceDocumentResponse, GetWorkspaceDocumentListResponse, } from "./types/workspaceDocument"; @@ -12,6 +13,10 @@ export const generateGetWorkspaceDocumentListQueryKey = (workspaceId: string) => return ["workspaces", workspaceId, "documents"]; }; +export const generateGetDocumentQueryKey = (workspaceId: string, documentId: string) => { + return ["workpsaces", workspaceId, "documents", documentId]; +}; + export const useGetWorkspaceDocumentListQuery = (workspaceId?: string) => { const query = useInfiniteQuery({ queryKey: generateGetWorkspaceDocumentListQueryKey(workspaceId || ""), @@ -36,6 +41,24 @@ export const useGetWorkspaceDocumentListQuery = (workspaceId?: string) => { return query; }; +export const useGetDocumentQuery = (workspaceId?: string | null, documentId?: string | null) => { + const query = useQuery({ + queryKey: generateGetDocumentQueryKey(workspaceId || "", documentId || ""), + enabled: Boolean(workspaceId && documentId), + queryFn: async () => { + const res = await axios.get( + `/workspaces/${workspaceId}/documents/${documentId}` + ); + return res.data; + }, + meta: { + errorMessage: "This is a non-existent or unauthorized document.", + }, + }); + + return query; +}; + export const useCreateDocumentMutation = (workspaceId: string) => { const queryClient = useQueryClient(); diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx index 997d2138..98f80765 100644 --- a/frontend/src/pages/document/Index.tsx +++ b/frontend/src/pages/document/Index.tsx @@ -14,8 +14,9 @@ import Resizable from "react-resizable-layout"; import { useWindowWidth } from "@react-hook/window-size"; import Preview from "../../components/editor/Preview"; import { useParams } from "react-router-dom"; -import { useGetDocumentQuery } from "../../hooks/api/document"; import { selectUser } from "../../store/userSlice"; +import { useGetDocumentQuery } from "../../hooks/api/workspaceDocument"; +import { useGetWorkspaceQuery } from "../../hooks/api/workspace"; function DocumentIndex() { const dispatch = useDispatch(); @@ -23,7 +24,8 @@ function DocumentIndex() { const userStore = useSelector(selectUser); const windowWidth = useWindowWidth(); const editorStore = useSelector(selectEditor); - const { data: document } = useGetDocumentQuery(params.documentId); + const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug); + const { data: document } = useGetDocumentQuery(workspace?.id, params.documentId); useEffect(() => { let client: yorkie.Client; From c1cb700f7099e8f2f19cf4b00a1e04f99e67d340 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:00:19 +0900 Subject: [PATCH 14/37] Change cleanup code for docs --- frontend/src/pages/document/Index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx index 98f80765..acd9f3c0 100644 --- a/frontend/src/pages/document/Index.tsx +++ b/frontend/src/pages/document/Index.tsx @@ -56,8 +56,11 @@ function DocumentIndex() { return () => { const cleanUp = async () => { - await client?.detach(doc); - await client?.deactivate(); + if (client.isActive()) { + await client.detach(doc); + await client.deactivate(); + } + dispatch(setDoc(null)); dispatch(setClient(null)); }; From 1138b53852cf2f81998c39bcfbe37cd2ba0fa7d7 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:08:38 +0900 Subject: [PATCH 15/37] Remove share mode in share modal --- frontend/src/components/modals/ShareModal.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/modals/ShareModal.tsx b/frontend/src/components/modals/ShareModal.tsx index e8d503da..5b78ed26 100644 --- a/frontend/src/components/modals/ShareModal.tsx +++ b/frontend/src/components/modals/ShareModal.tsx @@ -23,18 +23,16 @@ import clipboard from "clipboardy"; import { useSnackbar } from "notistack"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import CloseIcon from "@mui/icons-material/Close"; -import { useSearchParams } from "react-router-dom"; +import { useGetWorkspaceQuery } from "../../hooks/api/workspace"; interface ShareModalProps extends Omit {} function ShareModal(props: ShareModalProps) { const { ...modalProps } = props; const params = useParams(); - const [searchParams] = useSearchParams(); const [shareUrl, setShareUrl] = useState(null); - const { data: document } = useGetDocumentQuery( - searchParams.get("token") ? null : params.documentSlug - ); + const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug); + const { data: document } = useGetDocumentQuery(workspace?.id, params.documentId); const { mutateAsync: createWorkspaceSharingToken } = useCreateWorkspaceSharingTokenMutation( document?.workspaceId || "", document?.id || "" @@ -59,7 +57,7 @@ function ShareModal(props: ShareModalProps) { }); setShareUrl( - `${window.location.origin}/document/${params.documentSlug}?token=${sharingToken}` + `${window.location.origin}/${params.workspaceSlug}/${params.documentId}/share?token=${sharingToken}` ); }; @@ -137,7 +135,9 @@ function ShareModal(props: ShareModalProps) { {Boolean(shareUrl) && ( - {shareUrl} + + {shareUrl} + From 7858790f344bd259a637e8ff3fe0af394fd4d6e4 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:12:32 +0900 Subject: [PATCH 16/37] Add document share page --- .../src/pages/{ => workspace}/document/Index.tsx | 14 +++++++------- .../src/pages/workspace/document/share/Index.tsx | 7 +++++++ frontend/src/routes.tsx | 8 +++++++- 3 files changed, 21 insertions(+), 8 deletions(-) rename frontend/src/pages/{ => workspace}/document/Index.tsx (87%) create mode 100644 frontend/src/pages/workspace/document/share/Index.tsx diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/workspace/document/Index.tsx similarity index 87% rename from frontend/src/pages/document/Index.tsx rename to frontend/src/pages/workspace/document/Index.tsx index acd9f3c0..bf889e3e 100644 --- a/frontend/src/pages/document/Index.tsx +++ b/frontend/src/pages/workspace/document/Index.tsx @@ -1,22 +1,22 @@ import { useEffect } from "react"; -import Editor from "../../components/editor/Editor"; +import Editor from "../../../components/editor/Editor"; import * as yorkie from "yorkie-js-sdk"; -import { selectEditor, setClient, setDoc } from "../../store/editorSlice"; +import { selectEditor, setClient, setDoc } from "../../../store/editorSlice"; import { useDispatch, useSelector } from "react-redux"; import { YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType, -} from "../../utils/yorkie/yorkieSync"; +} from "../../../utils/yorkie/yorkieSync"; import randomColor from "randomcolor"; import Color from "color"; import { Box, Paper } from "@mui/material"; import Resizable from "react-resizable-layout"; import { useWindowWidth } from "@react-hook/window-size"; -import Preview from "../../components/editor/Preview"; +import Preview from "../../../components/editor/Preview"; import { useParams } from "react-router-dom"; -import { selectUser } from "../../store/userSlice"; -import { useGetDocumentQuery } from "../../hooks/api/workspaceDocument"; -import { useGetWorkspaceQuery } from "../../hooks/api/workspace"; +import { selectUser } from "../../../store/userSlice"; +import { useGetDocumentQuery } from "../../../hooks/api/workspaceDocument"; +import { useGetWorkspaceQuery } from "../../../hooks/api/workspace"; function DocumentIndex() { const dispatch = useDispatch(); diff --git a/frontend/src/pages/workspace/document/share/Index.tsx b/frontend/src/pages/workspace/document/share/Index.tsx new file mode 100644 index 00000000..dbbc5825 --- /dev/null +++ b/frontend/src/pages/workspace/document/share/Index.tsx @@ -0,0 +1,7 @@ +import { Box } from "@mui/material"; + +function DocumentShareIndex() { + return ; +} + +export default DocumentShareIndex; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 1d5c2fcd..e256ebf7 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -1,4 +1,4 @@ -import DocumentIndex from "./pages/document/Index"; +import DocumentIndex from "./pages/workspace/document/Index"; import MainLayout from "./components/layouts/MainLayout"; import LoginIndex from "./pages/login/Index"; import CallbackIndex from "./pages/auth/callback/Index"; @@ -10,6 +10,7 @@ import CodePairError from "./components/common/CodePairError"; import JoinIndex from "./pages/workspace/join/Index"; import Index from "./pages/Index"; import DocumentLayout from "./components/layouts/DocumentLayout"; +import DocumentShareIndex from "./pages/workspace/document/share/Index"; interface CodePairRoute { path: string; @@ -65,6 +66,11 @@ const codePairRoutes: Array = [ accessType: AccessType.PRIVATE, element: , }, + { + path: ":documentId/share", + accessType: AccessType.PUBLIC, + element: , + }, ], }, { From 01193171e2bfaa45f16bf5fdab624702e47123ed Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:22:16 +0900 Subject: [PATCH 17/37] Componentize DocumentView --- .../src/components/editor/DocumentView.tsx | 71 +++++++++++++++++++ .../src/pages/workspace/document/Index.tsx | 57 ++------------- .../pages/workspace/document/share/Index.tsx | 7 +- 3 files changed, 81 insertions(+), 54 deletions(-) create mode 100644 frontend/src/components/editor/DocumentView.tsx diff --git a/frontend/src/components/editor/DocumentView.tsx b/frontend/src/components/editor/DocumentView.tsx new file mode 100644 index 00000000..8f3c6d01 --- /dev/null +++ b/frontend/src/components/editor/DocumentView.tsx @@ -0,0 +1,71 @@ +import { useSelector } from "react-redux"; +import { selectEditor } from "../../store/editorSlice"; +import Resizable from "react-resizable-layout"; +import { useWindowWidth } from "@react-hook/window-size"; +import Editor from "./Editor"; +import { Backdrop, Box, CircularProgress, Paper } from "@mui/material"; +import Preview from "./Preview"; + +function DocumentView() { + const editorStore = useSelector(selectEditor); + const windowWidth = useWindowWidth(); + + if (!editorStore.doc || !editorStore.client) + return ( + + + + ); + + return ( + <> + {/* For Markdown Preview Theme */} +
+ {editorStore.mode === "both" && ( + + {({ position: width, separatorProps }) => ( +
+
+ +
+ +
+ + + +
+
+ )} +
+ )} + {editorStore.mode === "read" && ( + + + + )} + {editorStore.mode === "edit" && } + + ); +} + +export default DocumentView; diff --git a/frontend/src/pages/workspace/document/Index.tsx b/frontend/src/pages/workspace/document/Index.tsx index bf889e3e..56cacba0 100644 --- a/frontend/src/pages/workspace/document/Index.tsx +++ b/frontend/src/pages/workspace/document/Index.tsx @@ -1,7 +1,6 @@ import { useEffect } from "react"; -import Editor from "../../../components/editor/Editor"; import * as yorkie from "yorkie-js-sdk"; -import { selectEditor, setClient, setDoc } from "../../../store/editorSlice"; +import { setClient, setDoc } from "../../../store/editorSlice"; import { useDispatch, useSelector } from "react-redux"; import { YorkieCodeMirrorDocType, @@ -9,21 +8,17 @@ import { } from "../../../utils/yorkie/yorkieSync"; import randomColor from "randomcolor"; import Color from "color"; -import { Box, Paper } from "@mui/material"; -import Resizable from "react-resizable-layout"; -import { useWindowWidth } from "@react-hook/window-size"; -import Preview from "../../../components/editor/Preview"; +import { Box } from "@mui/material"; import { useParams } from "react-router-dom"; import { selectUser } from "../../../store/userSlice"; import { useGetDocumentQuery } from "../../../hooks/api/workspaceDocument"; import { useGetWorkspaceQuery } from "../../../hooks/api/workspace"; +import DocumentView from "../../../components/editor/DocumentView"; function DocumentIndex() { const dispatch = useDispatch(); const params = useParams(); const userStore = useSelector(selectUser); - const windowWidth = useWindowWidth(); - const editorStore = useSelector(selectEditor); const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug); const { data: document } = useGetDocumentQuery(workspace?.id, params.documentId); @@ -71,51 +66,7 @@ function DocumentIndex() { return ( - {/* For Markdown Preview Theme */} -
- {editorStore.mode === "both" && ( - - {({ position: width, separatorProps }) => ( -
-
- -
- -
- - - -
-
- )} -
- )} - {editorStore.mode === "read" && ( - - - - )} - {editorStore.mode === "edit" && } + ); } diff --git a/frontend/src/pages/workspace/document/share/Index.tsx b/frontend/src/pages/workspace/document/share/Index.tsx index dbbc5825..298e58af 100644 --- a/frontend/src/pages/workspace/document/share/Index.tsx +++ b/frontend/src/pages/workspace/document/share/Index.tsx @@ -1,7 +1,12 @@ import { Box } from "@mui/material"; +import DocumentView from "../../../../components/editor/DocumentView"; function DocumentShareIndex() { - return ; + return ( + + + + ); } export default DocumentShareIndex; From 1b2e9d2d590530b7ce698cb63ce02b312be58910 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:43:55 +0900 Subject: [PATCH 18/37] Add share mode --- frontend/src/hooks/useYorkieDocument.ts | 51 +++++++++++++++++++ .../pages/workspace/document/share/Index.tsx | 41 +++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 frontend/src/hooks/useYorkieDocument.ts diff --git a/frontend/src/hooks/useYorkieDocument.ts b/frontend/src/hooks/useYorkieDocument.ts new file mode 100644 index 00000000..a5717b06 --- /dev/null +++ b/frontend/src/hooks/useYorkieDocument.ts @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useState } from "react"; +import * as yorkie from "yorkie-js-sdk"; +import { YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType } from "../utils/yorkie/yorkieSync"; +import Color from "color"; +import randomColor from "randomcolor"; + +export const useYorkieDocument = ( + yorkieDocuentId?: string | null, + presenceName?: string | null +) => { + const [client, setClient] = useState(null); + const [doc, setDoc] = useState | null>(null); + const cleanUpYorkieDocument = useCallback(async () => { + if (!client || !doc) return; + + await client?.detach(doc); + await client?.deactivate(); + }, [client, doc]); + + useEffect(() => { + if (!yorkieDocuentId || !presenceName || doc || client) return; + + const initializeYorkie = async () => { + const newClient = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, { + apiKey: import.meta.env.VITE_YORKIE_API_KEY, + }); + await newClient.activate(); + + const newDoc = new yorkie.Document(yorkieDocuentId as string); + + await newClient.attach(newDoc, { + initialPresence: { + name: presenceName, + color: Color(randomColor()).fade(0.15).toString(), + selection: null, + }, + }); + + setClient(newClient); + setDoc( + newDoc as yorkie.Document + ); + }; + initializeYorkie(); + }, [presenceName, yorkieDocuentId, doc, client]); + + return { client, doc, cleanUpYorkieDocument }; +}; diff --git a/frontend/src/pages/workspace/document/share/Index.tsx b/frontend/src/pages/workspace/document/share/Index.tsx index 298e58af..3102c587 100644 --- a/frontend/src/pages/workspace/document/share/Index.tsx +++ b/frontend/src/pages/workspace/document/share/Index.tsx @@ -1,7 +1,48 @@ import { Box } from "@mui/material"; import DocumentView from "../../../../components/editor/DocumentView"; +import { useGetDocumentBySharingTokenQuery } from "../../../../hooks/api/document"; +import { Navigate, useLocation, useSearchParams } from "react-router-dom"; +import { useEffect, useMemo } from "react"; +import { useYorkieDocument } from "../../../../hooks/useYorkieDocument"; +import { useDispatch } from "react-redux"; +import { setClient, setDoc, setMode, setShareRole } from "../../../../store/editorSlice"; function DocumentShareIndex() { + const dispatch = useDispatch(); + const location = useLocation(); + const [searchParams] = useSearchParams(); + const shareToken = useMemo(() => searchParams.get("token"), [searchParams]); + const { data: sharedDocument } = useGetDocumentBySharingTokenQuery(shareToken); + const { doc, client, cleanUpYorkieDocument } = useYorkieDocument( + sharedDocument?.yorkieDocumentId, + "Anonymous" + ); + + useEffect(() => { + if (!sharedDocument?.role) return; + + dispatch(setShareRole(sharedDocument.role)); + + if (sharedDocument.role === "READ") { + dispatch(setMode("read")); + } + }, [dispatch, sharedDocument?.role]); + + useEffect(() => { + if (!doc || !client) return; + + dispatch(setDoc(doc)); + dispatch(setClient(client)); + + return () => { + cleanUpYorkieDocument(); + dispatch(setDoc(null)); + dispatch(setClient(null)); + }; + }, [cleanUpYorkieDocument, dispatch, client, doc]); + + if (!shareToken) return ; + return ( From 03c23da9fcee307ef66956e1ea1edea8654849ff Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:46:03 +0900 Subject: [PATCH 19/37] Fix mode button background padding --- .../src/components/headers/DocumentHeader.tsx | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/headers/DocumentHeader.tsx b/frontend/src/components/headers/DocumentHeader.tsx index 918418b2..16c2afc9 100644 --- a/frontend/src/components/headers/DocumentHeader.tsx +++ b/frontend/src/components/headers/DocumentHeader.tsx @@ -80,32 +80,32 @@ function DocumentHeader() { - + - {editorState.shareRole !== "READ" && ( - handleChangeMode(newMode)} - size="small" - > - - - - - - - - - - - - - - - - - )} + {/* {editorState.shareRole !== "READ" && ( */} + handleChangeMode(newMode)} + size="small" + > + + + + + + + + + + + + + + + + + {/* )} */} From de88e93238c72cd90cfe36494228ef7bfd38bec7 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:48:42 +0900 Subject: [PATCH 20/37] Change document page to use `useYorkieDocument` --- .../src/components/headers/DocumentHeader.tsx | 48 ++++++++--------- .../src/pages/workspace/document/Index.tsx | 54 +++++-------------- 2 files changed, 36 insertions(+), 66 deletions(-) diff --git a/frontend/src/components/headers/DocumentHeader.tsx b/frontend/src/components/headers/DocumentHeader.tsx index 16c2afc9..8db1d859 100644 --- a/frontend/src/components/headers/DocumentHeader.tsx +++ b/frontend/src/components/headers/DocumentHeader.tsx @@ -82,30 +82,30 @@ function DocumentHeader() { - {/* {editorState.shareRole !== "READ" && ( */} - handleChangeMode(newMode)} - size="small" - > - - - - - - - - - - - - - - - - - {/* )} */} + {editorState.shareRole !== "READ" && ( + handleChangeMode(newMode)} + size="small" + > + + + + + + + + + + + + + + + + + )} diff --git a/frontend/src/pages/workspace/document/Index.tsx b/frontend/src/pages/workspace/document/Index.tsx index 56cacba0..36d883c6 100644 --- a/frontend/src/pages/workspace/document/Index.tsx +++ b/frontend/src/pages/workspace/document/Index.tsx @@ -2,18 +2,13 @@ import { useEffect } from "react"; import * as yorkie from "yorkie-js-sdk"; import { setClient, setDoc } from "../../../store/editorSlice"; import { useDispatch, useSelector } from "react-redux"; -import { - YorkieCodeMirrorDocType, - YorkieCodeMirrorPresenceType, -} from "../../../utils/yorkie/yorkieSync"; -import randomColor from "randomcolor"; -import Color from "color"; import { Box } from "@mui/material"; import { useParams } from "react-router-dom"; import { selectUser } from "../../../store/userSlice"; import { useGetDocumentQuery } from "../../../hooks/api/workspaceDocument"; import { useGetWorkspaceQuery } from "../../../hooks/api/workspace"; import DocumentView from "../../../components/editor/DocumentView"; +import { useYorkieDocument } from "../../../hooks/useYorkieDocument"; function DocumentIndex() { const dispatch = useDispatch(); @@ -21,48 +16,23 @@ function DocumentIndex() { const userStore = useSelector(selectUser); const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug); const { data: document } = useGetDocumentQuery(workspace?.id, params.documentId); + const { doc, client, cleanUpYorkieDocument } = useYorkieDocument( + document?.yorkieDocumentId, + userStore.data?.nickname + ); useEffect(() => { - let client: yorkie.Client; - let doc: yorkie.Document; - const yorkieDocuentId = document?.yorkieDocumentId; - - if (!yorkieDocuentId) return; + if (!doc || !client) return; - const initializeYorkie = async () => { - client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, { - apiKey: import.meta.env.VITE_YORKIE_API_KEY, - }); - await client.activate(); - - doc = new yorkie.Document(yorkieDocuentId as string); - - await client.attach(doc, { - initialPresence: { - name: userStore.data?.nickname as string, - color: Color(randomColor()).fade(0.15).toString(), - selection: null, - }, - }); - dispatch(setDoc(doc)); - dispatch(setClient(client)); - }; - initializeYorkie(); + dispatch(setDoc(doc)); + dispatch(setClient(client)); return () => { - const cleanUp = async () => { - if (client.isActive()) { - await client.detach(doc); - await client.deactivate(); - } - - dispatch(setDoc(null)); - dispatch(setClient(null)); - }; - - cleanUp(); + cleanUpYorkieDocument(); + dispatch(setDoc(null)); + dispatch(setClient(null)); }; - }, [dispatch, document?.yorkieDocumentId, userStore.data?.nickname]); + }, [cleanUpYorkieDocument, dispatch, client, doc]); return ( From 4399da0a6cbabf6cbcd872d472952b03d532aee2 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:51:11 +0900 Subject: [PATCH 21/37] Add tooltip to avatar --- .../src/components/headers/DocumentHeader.tsx | 15 ++++++++------- frontend/src/pages/workspace/document/Index.tsx | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/headers/DocumentHeader.tsx b/frontend/src/components/headers/DocumentHeader.tsx index 8db1d859..86766803 100644 --- a/frontend/src/components/headers/DocumentHeader.tsx +++ b/frontend/src/components/headers/DocumentHeader.tsx @@ -111,13 +111,14 @@ function DocumentHeader() { {presenceList?.map((presence) => ( - - {presence.presence.name[0]} - + + + {presence.presence.name[0]} + + ))} {!editorState.shareRole && } diff --git a/frontend/src/pages/workspace/document/Index.tsx b/frontend/src/pages/workspace/document/Index.tsx index 36d883c6..b4aaa2b0 100644 --- a/frontend/src/pages/workspace/document/Index.tsx +++ b/frontend/src/pages/workspace/document/Index.tsx @@ -1,5 +1,4 @@ import { useEffect } from "react"; -import * as yorkie from "yorkie-js-sdk"; import { setClient, setDoc } from "../../../store/editorSlice"; import { useDispatch, useSelector } from "react-redux"; import { Box } from "@mui/material"; From 0856e7063197714b1447d40e3b8a5d32bc2253d8 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 17:55:07 +0900 Subject: [PATCH 22/37] Add back button to DocumentHeader --- frontend/src/components/headers/DocumentHeader.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/components/headers/DocumentHeader.tsx b/frontend/src/components/headers/DocumentHeader.tsx index 86766803..1cd81722 100644 --- a/frontend/src/components/headers/DocumentHeader.tsx +++ b/frontend/src/components/headers/DocumentHeader.tsx @@ -2,6 +2,7 @@ import { AppBar, Avatar, AvatarGroup, + IconButton, Paper, Stack, ToggleButton, @@ -20,9 +21,12 @@ import { useEffect } from "react"; import { useList } from "react-use"; import { ActorID } from "yorkie-js-sdk"; import { YorkieCodeMirrorPresenceType } from "../../utils/yorkie/yorkieSync"; +import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; +import { useNavigate } from "react-router-dom"; function DocumentHeader() { const dispatch = useDispatch(); + const navigate = useNavigate(); const editorState = useSelector(selectEditor); const [ presenceList, @@ -76,11 +80,20 @@ function DocumentHeader() { dispatch(setMode(newMode)); }; + const handleToPrevious = () => { + navigate(-1); + }; + return ( + + + + + {editorState.shareRole !== "READ" && ( Date: Wed, 24 Jan 2024 18:02:08 +0900 Subject: [PATCH 23/37] Remove slug in document db --- backend/prisma/schema.prisma | 1 - .../src/documents/types/document-domain.type.ts | 2 -- backend/src/users/users.service.ts | 2 +- .../workspace-documents.service.ts | 15 --------------- frontend/src/hooks/api/types/document.d.ts | 1 - 5 files changed, 1 insertion(+), 20 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 97bcda47..a7a16bfb 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -52,7 +52,6 @@ model Document { id String @id @default(auto()) @map("_id") @db.ObjectId yorkieDocumentId String @map("yorkie_document_id") title String - slug String content String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/backend/src/documents/types/document-domain.type.ts b/backend/src/documents/types/document-domain.type.ts index 9758695b..12157ff6 100644 --- a/backend/src/documents/types/document-domain.type.ts +++ b/backend/src/documents/types/document-domain.type.ts @@ -7,8 +7,6 @@ export class DocumentDomain { yorkieDocumentId: string; @ApiProperty({ type: String, description: "Title of the document" }) title: string; - @ApiProperty({ type: String, description: "Slug of the document" }) - slug: string; @ApiProperty({ type: String, description: "Content of the document", required: false }) content?: string; @ApiProperty({ type: Date, description: "Created date of the document" }) diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 5049c9d4..42615361 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -69,7 +69,7 @@ export class UsersService { }); const title = `${user.nickname}'s Workspace`; - let slug = slugify(title); + let slug = slugify(title, { lower: true }); const duplicatedWorkspaceList = await this.prismaService.workspace.findMany({ where: { diff --git a/backend/src/workspace-documents/workspace-documents.service.ts b/backend/src/workspace-documents/workspace-documents.service.ts index 111bc70e..997ef41a 100644 --- a/backend/src/workspace-documents/workspace-documents.service.ts +++ b/backend/src/workspace-documents/workspace-documents.service.ts @@ -23,24 +23,9 @@ export class WorkspaceDocumentsService { throw new NotFoundException(); } - let slug = slugify(title); - - const duplicatedDocumentList = await this.prismaService.document.findMany({ - where: { - slug: { - startsWith: slug, - }, - }, - }); - - if (duplicatedDocumentList.length) { - slug += `-${duplicatedDocumentList.length + 1}`; - } - return this.prismaService.document.create({ data: { title, - slug, workspaceId, yorkieDocumentId: Math.random().toString(36).substring(7), }, diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts index aac8faed..3e35f683 100644 --- a/frontend/src/hooks/api/types/document.d.ts +++ b/frontend/src/hooks/api/types/document.d.ts @@ -5,7 +5,6 @@ export class Document { workspaceId: string; yorkieDocumentId: string; title: string; - slug: string; content?: string; createdAt: Date; updatedAt: Date; From 0b9bea301d9e65954e7e9340483677577135815f Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:06:54 +0900 Subject: [PATCH 24/37] Fix lint --- backend/src/documents/documents.controller.ts | 3 +-- backend/src/workspace-documents/workspace-documents.service.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/documents/documents.controller.ts b/backend/src/documents/documents.controller.ts index b2cf380f..5ca1cc55 100644 --- a/backend/src/documents/documents.controller.ts +++ b/backend/src/documents/documents.controller.ts @@ -1,8 +1,7 @@ -import { Controller, Get, Param, Query, Req } from "@nestjs/common"; +import { Controller, Get, Query } from "@nestjs/common"; import { DocumentsService } from "./documents.service"; import { Public } from "src/utils/decorators/auth.decorator"; import { - ApiFoundResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, diff --git a/backend/src/workspace-documents/workspace-documents.service.ts b/backend/src/workspace-documents/workspace-documents.service.ts index 997ef41a..4e8814c9 100644 --- a/backend/src/workspace-documents/workspace-documents.service.ts +++ b/backend/src/workspace-documents/workspace-documents.service.ts @@ -4,7 +4,6 @@ import { PrismaService } from "src/db/prisma.service"; import { FindWorkspaceDocumentsResponse } from "./types/find-workspace-documents-response.type"; import { CreateWorkspaceDocumentShareTokenResponse } from "./types/create-workspace-document-share-token-response.type"; import { ShareRole } from "src/utils/types/share-role.type"; -import slugify from "slugify"; import { generateRandomKey } from "src/utils/functions/random-string"; @Injectable() From aad2ff7c089daf6f334666c06bf247fb1928c020 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:11:43 +0900 Subject: [PATCH 25/37] Add check path to API --- backend/src/app.module.ts | 2 ++ backend/src/check/check.controller.spec.ts | 18 ++++++++++++++++++ backend/src/check/check.controller.ts | 4 ++++ backend/src/check/check.module.ts | 9 +++++++++ backend/src/check/check.service.spec.ts | 18 ++++++++++++++++++ backend/src/check/check.service.ts | 4 ++++ 6 files changed, 55 insertions(+) create mode 100644 backend/src/check/check.controller.spec.ts create mode 100644 backend/src/check/check.controller.ts create mode 100644 backend/src/check/check.module.ts create mode 100644 backend/src/check/check.service.spec.ts create mode 100644 backend/src/check/check.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 50ba4575..acbebac7 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -9,6 +9,7 @@ import { WorkspacesModule } from "./workspaces/workspaces.module"; import { WorkspaceUsersModule } from "./workspace-users/workspace-users.module"; import { WorkspaceDocumentsModule } from "./workspace-documents/workspace-documents.module"; import { DocumentsModule } from "./documents/documents.module"; +import { CheckModule } from './check/check.module'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { DocumentsModule } from "./documents/documents.module"; WorkspaceUsersModule, WorkspaceDocumentsModule, DocumentsModule, + CheckModule, ], controllers: [], providers: [ diff --git a/backend/src/check/check.controller.spec.ts b/backend/src/check/check.controller.spec.ts new file mode 100644 index 00000000..5753bc5a --- /dev/null +++ b/backend/src/check/check.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CheckController } from './check.controller'; + +describe('CheckController', () => { + let controller: CheckController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CheckController], + }).compile(); + + controller = module.get(CheckController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/check/check.controller.ts b/backend/src/check/check.controller.ts new file mode 100644 index 00000000..1ef8045c --- /dev/null +++ b/backend/src/check/check.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('check') +export class CheckController {} diff --git a/backend/src/check/check.module.ts b/backend/src/check/check.module.ts new file mode 100644 index 00000000..75e50d9f --- /dev/null +++ b/backend/src/check/check.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CheckService } from './check.service'; +import { CheckController } from './check.controller'; + +@Module({ + providers: [CheckService], + controllers: [CheckController] +}) +export class CheckModule {} diff --git a/backend/src/check/check.service.spec.ts b/backend/src/check/check.service.spec.ts new file mode 100644 index 00000000..20e646b8 --- /dev/null +++ b/backend/src/check/check.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CheckService } from './check.service'; + +describe('CheckService', () => { + let service: CheckService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CheckService], + }).compile(); + + service = module.get(CheckService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/check/check.service.ts b/backend/src/check/check.service.ts new file mode 100644 index 00000000..a799f8b0 --- /dev/null +++ b/backend/src/check/check.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class CheckService {} From f6cee410d4dcd88d33bb23b24f330fee0360e42a Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:12:11 +0900 Subject: [PATCH 26/37] Fix formatting --- backend/src/app.module.ts | 2 +- backend/src/check/check.controller.spec.ts | 26 +++++++++++----------- backend/src/check/check.controller.ts | 4 ++-- backend/src/check/check.module.ts | 10 ++++----- backend/src/check/check.service.spec.ts | 26 +++++++++++----------- backend/src/check/check.service.ts | 2 +- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index acbebac7..2410d666 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -9,7 +9,7 @@ import { WorkspacesModule } from "./workspaces/workspaces.module"; import { WorkspaceUsersModule } from "./workspace-users/workspace-users.module"; import { WorkspaceDocumentsModule } from "./workspace-documents/workspace-documents.module"; import { DocumentsModule } from "./documents/documents.module"; -import { CheckModule } from './check/check.module'; +import { CheckModule } from "./check/check.module"; @Module({ imports: [ diff --git a/backend/src/check/check.controller.spec.ts b/backend/src/check/check.controller.spec.ts index 5753bc5a..1bd765cf 100644 --- a/backend/src/check/check.controller.spec.ts +++ b/backend/src/check/check.controller.spec.ts @@ -1,18 +1,18 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CheckController } from './check.controller'; +import { Test, TestingModule } from "@nestjs/testing"; +import { CheckController } from "./check.controller"; -describe('CheckController', () => { - let controller: CheckController; +describe("CheckController", () => { + let controller: CheckController; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CheckController], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CheckController], + }).compile(); - controller = module.get(CheckController); - }); + controller = module.get(CheckController); + }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + it("should be defined", () => { + expect(controller).toBeDefined(); + }); }); diff --git a/backend/src/check/check.controller.ts b/backend/src/check/check.controller.ts index 1ef8045c..52d4f56f 100644 --- a/backend/src/check/check.controller.ts +++ b/backend/src/check/check.controller.ts @@ -1,4 +1,4 @@ -import { Controller } from '@nestjs/common'; +import { Controller } from "@nestjs/common"; -@Controller('check') +@Controller("check") export class CheckController {} diff --git a/backend/src/check/check.module.ts b/backend/src/check/check.module.ts index 75e50d9f..80a2f3ea 100644 --- a/backend/src/check/check.module.ts +++ b/backend/src/check/check.module.ts @@ -1,9 +1,9 @@ -import { Module } from '@nestjs/common'; -import { CheckService } from './check.service'; -import { CheckController } from './check.controller'; +import { Module } from "@nestjs/common"; +import { CheckService } from "./check.service"; +import { CheckController } from "./check.controller"; @Module({ - providers: [CheckService], - controllers: [CheckController] + providers: [CheckService], + controllers: [CheckController], }) export class CheckModule {} diff --git a/backend/src/check/check.service.spec.ts b/backend/src/check/check.service.spec.ts index 20e646b8..8d4b4c8d 100644 --- a/backend/src/check/check.service.spec.ts +++ b/backend/src/check/check.service.spec.ts @@ -1,18 +1,18 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CheckService } from './check.service'; +import { Test, TestingModule } from "@nestjs/testing"; +import { CheckService } from "./check.service"; -describe('CheckService', () => { - let service: CheckService; +describe("CheckService", () => { + let service: CheckService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CheckService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CheckService], + }).compile(); - service = module.get(CheckService); - }); + service = module.get(CheckService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it("should be defined", () => { + expect(service).toBeDefined(); + }); }); diff --git a/backend/src/check/check.service.ts b/backend/src/check/check.service.ts index a799f8b0..cafe93f4 100644 --- a/backend/src/check/check.service.ts +++ b/backend/src/check/check.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable } from "@nestjs/common"; @Injectable() export class CheckService {} From ce61e09931ebce57b1e3f565f0053b5adc9f4f51 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:27:31 +0900 Subject: [PATCH 27/37] Add name conflict checking API --- backend/src/check/check.controller.ts | 26 +++++++++++++++++-- backend/src/check/check.module.ts | 3 ++- backend/src/check/check.service.ts | 23 +++++++++++++++- .../src/check/dto/check-name-conflict.dto.ts | 6 +++++ .../check-name-conflict-response.type.ts | 6 +++++ 5 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 backend/src/check/dto/check-name-conflict.dto.ts create mode 100644 backend/src/check/types/check-name-conflict-response.type.ts diff --git a/backend/src/check/check.controller.ts b/backend/src/check/check.controller.ts index 52d4f56f..c914bccf 100644 --- a/backend/src/check/check.controller.ts +++ b/backend/src/check/check.controller.ts @@ -1,4 +1,26 @@ -import { Controller } from "@nestjs/common"; +import { Controller, Post } from "@nestjs/common"; +import { CheckService } from "./check.service"; +import { CheckNameConflictDto } from "./dto/check-name-conflict.dto"; +import { CheckNameConflicReponse } from "./types/check-name-conflict-response.type"; +import { Public } from "src/utils/decorators/auth.decorator"; +import { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; +@ApiTags("Check") @Controller("check") -export class CheckController {} +export class CheckController { + constructor(private checkService: CheckService) {} + + @Public() + @Post("name-conflicts") + @ApiOperation({ + summary: "Check Whether The Name Conflicts with Username or Title of Workspace.", + description: "If the name is conflict, it returns true.", + }) + @ApiBody({ type: CheckNameConflictDto }) + @ApiOkResponse({ type: CheckNameConflicReponse }) + async checkNameConflict( + checkNameConflictDto: CheckNameConflictDto + ): Promise { + return this.checkService.checkNameConflict(checkNameConflictDto.name); + } +} diff --git a/backend/src/check/check.module.ts b/backend/src/check/check.module.ts index 80a2f3ea..ce15f8cb 100644 --- a/backend/src/check/check.module.ts +++ b/backend/src/check/check.module.ts @@ -1,9 +1,10 @@ import { Module } from "@nestjs/common"; import { CheckService } from "./check.service"; import { CheckController } from "./check.controller"; +import { PrismaService } from "src/db/prisma.service"; @Module({ - providers: [CheckService], + providers: [CheckService, PrismaService], controllers: [CheckController], }) export class CheckModule {} diff --git a/backend/src/check/check.service.ts b/backend/src/check/check.service.ts index cafe93f4..42d5b2f5 100644 --- a/backend/src/check/check.service.ts +++ b/backend/src/check/check.service.ts @@ -1,4 +1,25 @@ import { Injectable } from "@nestjs/common"; +import { PrismaService } from "src/db/prisma.service"; +import { CheckNameConflicReponse } from "./types/check-name-conflict-response.type"; @Injectable() -export class CheckService {} +export class CheckService { + constructor(private prismaService: PrismaService) {} + + async checkNameConflict(name: string): Promise { + const conflictUserList = await this.prismaService.user.findMany({ + where: { + nickname: name, + }, + }); + const conflictWorkspaceList = await this.prismaService.workspace.findMany({ + where: { + title: name, + }, + }); + + return { + conflict: Boolean(conflictUserList.length + conflictWorkspaceList.length), + }; + } +} diff --git a/backend/src/check/dto/check-name-conflict.dto.ts b/backend/src/check/dto/check-name-conflict.dto.ts new file mode 100644 index 00000000..b9a5586e --- /dev/null +++ b/backend/src/check/dto/check-name-conflict.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class CheckNameConflictDto { + @ApiProperty({ type: String, description: "Name to check conflict" }) + name: string; +} diff --git a/backend/src/check/types/check-name-conflict-response.type.ts b/backend/src/check/types/check-name-conflict-response.type.ts new file mode 100644 index 00000000..4d8feba4 --- /dev/null +++ b/backend/src/check/types/check-name-conflict-response.type.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class CheckNameConflicReponse { + @ApiProperty({ type: Boolean, description: "Whether the name is conflict" }) + conflict: boolean; +} From d9762ea08be99a5f124d31d51fc6745b89075c54 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:28:03 +0900 Subject: [PATCH 28/37] Fix formatting --- backend/tsconfig.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 95f5641c..8a022302 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,21 +1,21 @@ { - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false - } + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + }, } From e7472e563a4914b6a5047bd7269450b7259b8751 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:34:32 +0900 Subject: [PATCH 29/37] Remove default nickname --- backend/prisma/schema.prisma | 2 +- backend/src/auth/auth.controller.ts | 3 +- backend/src/auth/auth.service.ts | 13 ------- backend/src/users/types/user-domain.type.ts | 8 ++-- backend/src/users/users.service.ts | 41 +------------------- backend/src/workspaces/workspaces.service.ts | 2 +- frontend/src/hooks/api/types/user.d.ts | 4 +- 7 files changed, 11 insertions(+), 62 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index a7a16bfb..406ec66e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -14,7 +14,7 @@ model User { id String @id @default(auto()) @map("_id") @db.ObjectId socialProvider String @map("social_provider") socialUid String @unique @map("social_uid") - nickname String + nickname String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") userWorkspaceList UserWorkspace[] diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index a6eb4657..9bfc0215 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -30,8 +30,7 @@ export class AuthController { async login(@Req() req: LoginRequest): Promise { const user = await this.usersService.findOrCreate( req.user.socialProvider, - req.user.socialUid, - req.user.nickname + req.user.socialUid ); const accessToken = this.jwtService.sign({ sub: user.id, nickname: user.nickname }); diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 3bff1d09..beddfe5b 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -4,17 +4,4 @@ import { UsersService } from "src/users/users.service"; @Injectable() export class AuthService { constructor(private usersService: UsersService) {} - - async issueJwtToken(socialProvider: string, socialUid: string, nickname: string) { - const user = await this.usersService.findOrCreate(socialProvider, socialUid, nickname); - - if (user) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { socialProvider: _socaialProvider, socialUid: _social, ...result } = user; - - return result; - } - - return null; - } } diff --git a/backend/src/users/types/user-domain.type.ts b/backend/src/users/types/user-domain.type.ts index 57f4be2f..321cf272 100644 --- a/backend/src/users/types/user-domain.type.ts +++ b/backend/src/users/types/user-domain.type.ts @@ -3,10 +3,10 @@ import { ApiProperty } from "@nestjs/swagger"; export class UserDomain { @ApiProperty({ type: String, description: "ID of user" }) id: string; - @ApiProperty({ type: String, description: "Nickname of user" }) - nickname: string; - @ApiProperty({ type: String, description: "Last worksace slug of user" }) - lastWorkspaceSlug: string; + @ApiProperty({ type: String, description: "Nickname of user", required: false }) + nickname?: string; + @ApiProperty({ type: String, description: "Last worksace slug of user", required: false }) + lastWorkspaceSlug?: string; @ApiProperty({ type: Date, description: "Created date of user" }) createdAt: Date; @ApiProperty({ type: Date, description: "Updated date of user" }) diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 42615361..187960f9 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -2,8 +2,6 @@ import { Injectable } from "@nestjs/common"; import { User } from "@prisma/client"; import { PrismaService } from "src/db/prisma.service"; import { FindUserResponse } from "./types/find-user-response.type"; -import { WorkspaceRoleConstants } from "src/utils/constants/auth-role"; -import slugify from "slugify"; @Injectable() export class UsersService { @@ -40,15 +38,11 @@ export class UsersService { return { ...foundUser, - lastWorkspaceSlug: foundUserWorkspace.workspace.slug, + lastWorkspaceSlug: foundUserWorkspace?.workspace?.slug, }; } - async findOrCreate( - socialProvider: string, - socialUid: string, - nickname: string - ): Promise { + async findOrCreate(socialProvider: string, socialUid: string): Promise { const foundUser = await this.prismaService.user.findFirst({ where: { socialProvider, @@ -64,37 +58,6 @@ export class UsersService { data: { socialProvider, socialUid, - nickname, - }, - }); - - const title = `${user.nickname}'s Workspace`; - let slug = slugify(title, { lower: true }); - - const duplicatedWorkspaceList = await this.prismaService.workspace.findMany({ - where: { - slug: { - startsWith: slug, - }, - }, - }); - - if (duplicatedWorkspaceList.length) { - slug += `-${duplicatedWorkspaceList.length + 1}`; - } - - const workspace = await this.prismaService.workspace.create({ - data: { - title, - slug, - }, - }); - - await this.prismaService.userWorkspace.create({ - data: { - userId: user.id, - workspaceId: workspace.id, - role: WorkspaceRoleConstants.OWNER, }, }); diff --git a/backend/src/workspaces/workspaces.service.ts b/backend/src/workspaces/workspaces.service.ts index ff379b7a..0b0c5f06 100644 --- a/backend/src/workspaces/workspaces.service.ts +++ b/backend/src/workspaces/workspaces.service.ts @@ -13,7 +13,7 @@ export class WorkspacesService { constructor(private prismaService: PrismaService) {} async create(userId: string, title: string): Promise { - let slug = slugify(title); + let slug = slugify(title, { lower: true }); const duplicatedWorkspaceList = await this.prismaService.workspace.findMany({ where: { diff --git a/frontend/src/hooks/api/types/user.d.ts b/frontend/src/hooks/api/types/user.d.ts index 17fe7cb6..48dfadba 100644 --- a/frontend/src/hooks/api/types/user.d.ts +++ b/frontend/src/hooks/api/types/user.d.ts @@ -1,7 +1,7 @@ export interface User { id: string; - nickname: string; - lastWorkspaceSlug: string; + nickname?: string | null; + lastWorkspaceSlug?: string | null; createdAt: Date; updatedAt: Date; } From fd81ffc1b7d1618cf24a698256360b9d58bfa8ca Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:51:08 +0900 Subject: [PATCH 30/37] Add API for changing nickname --- backend/src/users/dto/change-nickname.dto.ts | 6 +++ backend/src/users/users.controller.ts | 30 ++++++++++- backend/src/users/users.module.ts | 3 +- backend/src/users/users.service.ts | 56 +++++++++++++++++++- 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 backend/src/users/dto/change-nickname.dto.ts diff --git a/backend/src/users/dto/change-nickname.dto.ts b/backend/src/users/dto/change-nickname.dto.ts new file mode 100644 index 00000000..ee610194 --- /dev/null +++ b/backend/src/users/dto/change-nickname.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class ChangeNicknameDto { + @ApiProperty({ type: String, description: "Nickname of user to update" }) + nickname: string; +} diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts index 9b2c9d72..0b9708dc 100644 --- a/backend/src/users/users.controller.ts +++ b/backend/src/users/users.controller.ts @@ -1,8 +1,17 @@ -import { Controller, Get, Req } from "@nestjs/common"; -import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { Body, Controller, Get, Put, Req } from "@nestjs/common"; +import { + ApiBearerAuth, + ApiBody, + ApiConflictResponse, + ApiOkResponse, + ApiOperation, + ApiResponse, + ApiTags, +} from "@nestjs/swagger"; import { UsersService } from "./users.service"; import { AuthroizedRequest } from "src/utils/types/req.type"; import { FindUserResponse } from "./types/find-user-response.type"; +import { ChangeNicknameDto } from "./dto/change-nickname.dto"; @ApiTags("Users") @ApiBearerAuth() @@ -19,4 +28,21 @@ export class UsersController { async findOne(@Req() req: AuthroizedRequest): Promise { return this.usersService.findOne(req.user.id); } + + @Put("") + @ApiOperation({ + summary: "Change the Nickname of the User", + description: "Change the nickname of the user", + }) + @ApiBody({ + type: ChangeNicknameDto, + }) + @ApiOkResponse() + @ApiConflictResponse({ description: "The nickname conflicts" }) + async changeNickname( + @Req() req: AuthroizedRequest, + @Body() changeNicknameDto: ChangeNicknameDto + ): Promise { + return this.usersService.changeNickname(req.user.id, changeNicknameDto.nickname); + } } diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index 83077360..7930a041 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -2,9 +2,10 @@ import { Module } from "@nestjs/common"; import { UsersService } from "./users.service"; import { PrismaService } from "src/db/prisma.service"; import { UsersController } from "./users.controller"; +import { CheckService } from "src/check/check.service"; @Module({ - providers: [UsersService, PrismaService], + providers: [UsersService, PrismaService, CheckService], exports: [UsersService], controllers: [UsersController], }) diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 187960f9..b0d74102 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -1,11 +1,17 @@ -import { Injectable } from "@nestjs/common"; +import { ConflictException, Injectable } from "@nestjs/common"; import { User } from "@prisma/client"; import { PrismaService } from "src/db/prisma.service"; import { FindUserResponse } from "./types/find-user-response.type"; +import { CheckService } from "src/check/check.service"; +import slugify from "slugify"; +import { WorkspaceRoleConstants } from "src/utils/constants/auth-role"; @Injectable() export class UsersService { - constructor(private prismaService: PrismaService) {} + constructor( + private prismaService: PrismaService, + private checkService: CheckService + ) {} async findOne(userId: string): Promise { const foundUserWorkspace = await this.prismaService.userWorkspace.findFirst({ @@ -63,4 +69,50 @@ export class UsersService { return user; } + + async changeNickname(userId: string, nickname: string): Promise { + const { conflict } = await this.checkService.checkNameConflict(nickname); + + if (conflict) { + throw new ConflictException(); + } + + await this.prismaService.user.update({ + where: { + id: userId, + }, + data: { + nickname, + }, + }); + + const userWorkspaceList = await this.prismaService.userWorkspace.findMany({ + select: {}, + where: { + userId, + }, + }); + + const slug = slugify(nickname, { lower: true }); + + if (!userWorkspaceList.length) { + const { id: workspaceId } = await this.prismaService.workspace.create({ + select: { + id: true, + }, + data: { + title: nickname, + slug, + }, + }); + + await this.prismaService.userWorkspace.create({ + data: { + workspaceId, + userId, + role: WorkspaceRoleConstants.OWNER, + }, + }); + } + } } From 108d260ff689b0dbec0328fa8de675bf4c6098ca Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:54:48 +0900 Subject: [PATCH 31/37] Change findOptions for user workspaces --- backend/src/users/users.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index b0d74102..04de8452 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -87,7 +87,9 @@ export class UsersService { }); const userWorkspaceList = await this.prismaService.userWorkspace.findMany({ - select: {}, + select: { + id: true, + }, where: { userId, }, From a14d5547733b0896b5fc57180ce60eae37882870 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 18:56:24 +0900 Subject: [PATCH 32/37] Fix lint --- backend/src/users/users.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts index 0b9708dc..4397a48e 100644 --- a/backend/src/users/users.controller.ts +++ b/backend/src/users/users.controller.ts @@ -5,7 +5,6 @@ import { ApiConflictResponse, ApiOkResponse, ApiOperation, - ApiResponse, ApiTags, } from "@nestjs/swagger"; import { UsersService } from "./users.service"; From 2a65fbf8913a57a9b5f278d137eed05ea5867328 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 21:56:31 +0900 Subject: [PATCH 33/37] Add Change nickname modal --- backend/src/check/check.controller.ts | 6 +- .../components/drawers/WorkspaceDrawer.tsx | 2 +- .../components/modals/ChangeNicknameModal.tsx | 88 +++++++++++++++++++ frontend/src/hooks/api/check.ts | 22 +++++ frontend/src/hooks/api/types/check.d.ts | 7 ++ frontend/src/hooks/api/types/user.d.ts | 6 +- frontend/src/hooks/api/user.ts | 22 ++++- frontend/src/providers/AuthProvider.tsx | 11 ++- frontend/src/store/userSlice.ts | 2 +- 9 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/modals/ChangeNicknameModal.tsx create mode 100644 frontend/src/hooks/api/check.ts create mode 100644 frontend/src/hooks/api/types/check.d.ts diff --git a/backend/src/check/check.controller.ts b/backend/src/check/check.controller.ts index c914bccf..561896d4 100644 --- a/backend/src/check/check.controller.ts +++ b/backend/src/check/check.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post } from "@nestjs/common"; +import { Body, Controller, Post } from "@nestjs/common"; import { CheckService } from "./check.service"; import { CheckNameConflictDto } from "./dto/check-name-conflict.dto"; import { CheckNameConflicReponse } from "./types/check-name-conflict-response.type"; @@ -11,7 +11,7 @@ export class CheckController { constructor(private checkService: CheckService) {} @Public() - @Post("name-conflicts") + @Post("name-conflict") @ApiOperation({ summary: "Check Whether The Name Conflicts with Username or Title of Workspace.", description: "If the name is conflict, it returns true.", @@ -19,7 +19,7 @@ export class CheckController { @ApiBody({ type: CheckNameConflictDto }) @ApiOkResponse({ type: CheckNameConflicReponse }) async checkNameConflict( - checkNameConflictDto: CheckNameConflictDto + @Body() checkNameConflictDto: CheckNameConflictDto ): Promise { return this.checkService.checkNameConflict(checkNameConflictDto.name); } diff --git a/frontend/src/components/drawers/WorkspaceDrawer.tsx b/frontend/src/components/drawers/WorkspaceDrawer.tsx index 94f259c1..2655b7bd 100644 --- a/frontend/src/components/drawers/WorkspaceDrawer.tsx +++ b/frontend/src/components/drawers/WorkspaceDrawer.tsx @@ -143,7 +143,7 @@ function WorkspaceDrawer() { - {userStore.data?.nickname.charAt(0)} + {userStore.data?.nickname?.charAt(0)} diff --git a/frontend/src/components/modals/ChangeNicknameModal.tsx b/frontend/src/components/modals/ChangeNicknameModal.tsx new file mode 100644 index 00000000..11600c9a --- /dev/null +++ b/frontend/src/components/modals/ChangeNicknameModal.tsx @@ -0,0 +1,88 @@ +import { Button, FormControl, Modal, ModalProps, Paper, Stack, Typography } from "@mui/material"; +import { FormContainer, TextFieldElement } from "react-hook-form-mui"; +import { useCheckNameConflictQuery } from "../../hooks/api/check"; +import { useMemo, useState } from "react"; +import { useDebounce } from "react-use"; +import { useUpdateUserNicknmaeMutation } from "../../hooks/api/user"; + +interface ChangeNicknameModalProps extends Omit {} + +function ChangeNicknameModal(props: ChangeNicknameModalProps) { + const [nickname, setNickname] = useState(""); + const [debouncedNickname, setDebouncedNickname] = useState(""); + const { data: conflictResult } = useCheckNameConflictQuery(debouncedNickname); + const { mutateAsync: updateUserNickname } = useUpdateUserNicknmaeMutation(); + const errorMessage = useMemo(() => { + if (conflictResult?.conflict) { + return "Already Exists"; + } + return null; + }, [conflictResult?.conflict]); + + useDebounce( + () => { + setDebouncedNickname(nickname); + }, + 500, + [nickname] + ); + + const handleNicknameChange = (e: React.ChangeEvent) => { + setNickname(e.target.value); + }; + + const handleUpdateUserNickname = async (data: { nickname: string }) => { + await updateUserNickname(data); + }; + + return ( + + + + Create Your Nickname + + + + + + + + + + + + ); +} + +export default ChangeNicknameModal; diff --git a/frontend/src/hooks/api/check.ts b/frontend/src/hooks/api/check.ts new file mode 100644 index 00000000..908bf834 --- /dev/null +++ b/frontend/src/hooks/api/check.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { CheckNameConflictRequest, CheckNameConflictResponse } from "./types/check"; + +export const generateCheckNameConflictQueryKey = (name: string) => { + return ["check", "name-conflict", name]; +}; + +export const useCheckNameConflictQuery = (name: string | null) => { + const query = useQuery({ + queryKey: generateCheckNameConflictQueryKey(name || ""), + enabled: Boolean(name), + queryFn: async () => { + const res = await axios.post("/check/name-conflict", { + name, + } as CheckNameConflictRequest); + return res.data; + }, + }); + + return query; +}; diff --git a/frontend/src/hooks/api/types/check.d.ts b/frontend/src/hooks/api/types/check.d.ts new file mode 100644 index 00000000..aedaa54a --- /dev/null +++ b/frontend/src/hooks/api/types/check.d.ts @@ -0,0 +1,7 @@ +export class CheckNameConflictRequest { + name: string; +} + +export class CheckNameConflictResponse { + conflict: boolean; +} diff --git a/frontend/src/hooks/api/types/user.d.ts b/frontend/src/hooks/api/types/user.d.ts index 48dfadba..cc647353 100644 --- a/frontend/src/hooks/api/types/user.d.ts +++ b/frontend/src/hooks/api/types/user.d.ts @@ -1,4 +1,4 @@ -export interface User { +export class User { id: string; nickname?: string | null; lastWorkspaceSlug?: string | null; @@ -7,3 +7,7 @@ export interface User { } export class GetUserResponse extends User {} + +export class UpdateUserRequest { + nickname: string; +} diff --git a/frontend/src/hooks/api/user.ts b/frontend/src/hooks/api/user.ts index 902f0f5f..c5011970 100644 --- a/frontend/src/hooks/api/user.ts +++ b/frontend/src/hooks/api/user.ts @@ -1,8 +1,8 @@ import { useDispatch, useSelector } from "react-redux"; import { selectAuth, setAccessToken } from "../../store/authSlice"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; -import { GetUserResponse } from "./types/user"; +import { GetUserResponse, UpdateUserRequest } from "./types/user"; import { useEffect } from "react"; import { User, setUserData } from "../../store/userSlice"; @@ -39,3 +39,21 @@ export const useGetUserQuery = () => { return query; }; + +export const useUpdateUserNicknmaeMutation = () => { + const authStore = useSelector(selectAuth); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: UpdateUserRequest) => { + const res = await axios.put("/users", data); + + return res.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: generateGetUserQueryKey(authStore.accessToken || ""), + }); + }, + }); +}; diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index ef2fd8ab..1a1be25c 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -1,6 +1,7 @@ -import { ReactNode } from "react"; +import { ReactNode, useMemo } from "react"; import { AuthContext } from "../contexts/AuthContext"; import { useGetUserQuery } from "../hooks/api/user"; +import ChangeNicknameModal from "../components/modals/ChangeNicknameModal"; interface AuthProviderProps { children?: ReactNode; @@ -8,11 +9,15 @@ interface AuthProviderProps { function AuthProvider(props: AuthProviderProps) { const { children } = props; - const { isSuccess, isLoading } = useGetUserQuery(); + const { data: user, isSuccess, isLoading } = useGetUserQuery(); + const shouldChangeNickname = useMemo( + () => isSuccess && !user.nickname, + [isSuccess, user?.nickname] + ); return ( - {children} + {shouldChangeNickname ? : children} ); } diff --git a/frontend/src/store/userSlice.ts b/frontend/src/store/userSlice.ts index 41ee044d..a38540ed 100644 --- a/frontend/src/store/userSlice.ts +++ b/frontend/src/store/userSlice.ts @@ -4,7 +4,7 @@ import { RootState } from "./store"; export interface User { id: string; - nickname: string; + nickname: string | null; lastWorkspaceSlug: string; updatedAt: Date; createdAt: Date; From 0eabbb595da64a3d6d0a05cf18665ba534a8b779 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 22:00:55 +0900 Subject: [PATCH 34/37] Add name conflict checking on workspace --- backend/src/check/check.service.ts | 6 ++-- backend/src/workspaces/workspaces.module.ts | 3 +- backend/src/workspaces/workspaces.service.ts | 29 ++++++++++---------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/backend/src/check/check.service.ts b/backend/src/check/check.service.ts index 42d5b2f5..daa73e2f 100644 --- a/backend/src/check/check.service.ts +++ b/backend/src/check/check.service.ts @@ -1,20 +1,22 @@ import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/db/prisma.service"; import { CheckNameConflicReponse } from "./types/check-name-conflict-response.type"; +import slugify from "slugify"; @Injectable() export class CheckService { constructor(private prismaService: PrismaService) {} async checkNameConflict(name: string): Promise { + const slug = slugify(name, { lower: true }); const conflictUserList = await this.prismaService.user.findMany({ where: { - nickname: name, + OR: [{ nickname: name }, { nickname: slug }], }, }); const conflictWorkspaceList = await this.prismaService.workspace.findMany({ where: { - title: name, + OR: [{ title: name }, { title: slug }], }, }); diff --git a/backend/src/workspaces/workspaces.module.ts b/backend/src/workspaces/workspaces.module.ts index 99ef2608..e6548035 100644 --- a/backend/src/workspaces/workspaces.module.ts +++ b/backend/src/workspaces/workspaces.module.ts @@ -2,10 +2,11 @@ import { Module } from "@nestjs/common"; import { WorkspacesController } from "./workspaces.controller"; import { WorkspacesService } from "./workspaces.service"; import { PrismaService } from "src/db/prisma.service"; +import { CheckService } from "src/check/check.service"; @Module({ imports: [], controllers: [WorkspacesController], - providers: [WorkspacesService, PrismaService], + providers: [WorkspacesService, PrismaService, CheckService], }) export class WorkspacesModule {} diff --git a/backend/src/workspaces/workspaces.service.ts b/backend/src/workspaces/workspaces.service.ts index 0b0c5f06..978f66d0 100644 --- a/backend/src/workspaces/workspaces.service.ts +++ b/backend/src/workspaces/workspaces.service.ts @@ -1,4 +1,9 @@ -import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common"; +import { + ConflictException, + Injectable, + NotFoundException, + UnauthorizedException, +} from "@nestjs/common"; import { Prisma, Workspace } from "@prisma/client"; import { PrismaService } from "src/db/prisma.service"; import { FindWorkspacesResponse } from "./types/find-workspaces-response.type"; @@ -7,30 +12,26 @@ import { WorkspaceRoleConstants } from "src/utils/constants/auth-role"; import slugify from "slugify"; import { generateRandomKey } from "src/utils/functions/random-string"; import * as moment from "moment"; +import { CheckService } from "src/check/check.service"; @Injectable() export class WorkspacesService { - constructor(private prismaService: PrismaService) {} + constructor( + private prismaService: PrismaService, + private checkService: CheckService + ) {} async create(userId: string, title: string): Promise { - let slug = slugify(title, { lower: true }); + const { conflict } = await this.checkService.checkNameConflict(title); - const duplicatedWorkspaceList = await this.prismaService.workspace.findMany({ - where: { - slug: { - startsWith: slug, - }, - }, - }); - - if (duplicatedWorkspaceList.length) { - slug += `-${duplicatedWorkspaceList.length + 1}`; + if (conflict) { + throw new ConflictException(); } const workspace = await this.prismaService.workspace.create({ data: { title, - slug, + slug: slugify(title, { lower: true }), }, }); From d208591b90c0331144c80e000c835aae4d869cc2 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 22:05:15 +0900 Subject: [PATCH 35/37] Add conflict checking to CreateModal --- .../src/components/modals/CreateModal.tsx | 38 ++++++++++++++++++- .../popovers/WorkspaceListPopover.tsx | 1 + 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/modals/CreateModal.tsx b/frontend/src/components/modals/CreateModal.tsx index a0ab2913..bf3fd712 100644 --- a/frontend/src/components/modals/CreateModal.tsx +++ b/frontend/src/components/modals/CreateModal.tsx @@ -10,6 +10,9 @@ import { } from "@mui/material"; import { FormContainer, TextFieldElement } from "react-hook-form-mui"; import CloseIcon from "@mui/icons-material/Close"; +import { useMemo, useState } from "react"; +import { useCheckNameConflictQuery } from "../../hooks/api/check"; +import { useDebounce } from "react-use"; interface CreateRequest { title: string; @@ -18,10 +21,28 @@ interface CreateRequest { interface CreateModalProps extends Omit { title: string; onSuccess: (data: CreateRequest) => Promise; + enableConflictCheck?: boolean; } function CreateModal(props: CreateModalProps) { - const { title, onSuccess, ...modalProps } = props; + const { title, onSuccess, enableConflictCheck, ...modalProps } = props; + const [nickname, setNickname] = useState(""); + const [debouncedNickname, setDebouncedNickname] = useState(""); + const { data: conflictResult } = useCheckNameConflictQuery(debouncedNickname); + const errorMessage = useMemo(() => { + if (conflictResult?.conflict) { + return "Already Exists"; + } + return null; + }, [conflictResult?.conflict]); + + useDebounce( + () => { + setDebouncedNickname(nickname); + }, + 500, + [nickname] + ); const handleCloseModal = () => { modalProps?.onClose?.(new Event("Close Modal"), "escapeKeyDown"); @@ -32,6 +53,11 @@ function CreateModal(props: CreateModalProps) { handleCloseModal(); }; + const handleNicknameChange = (e: React.ChangeEvent) => { + if (!enableConflictCheck) return; + setNickname(e.target.value); + }; + return ( - diff --git a/frontend/src/components/popovers/WorkspaceListPopover.tsx b/frontend/src/components/popovers/WorkspaceListPopover.tsx index d25b9839..d6614dc8 100644 --- a/frontend/src/components/popovers/WorkspaceListPopover.tsx +++ b/frontend/src/components/popovers/WorkspaceListPopover.tsx @@ -129,6 +129,7 @@ function WorkspaceListPopover(props: WorkspaceListPopoverProps) { title="Workspace" onClose={handleCreateWorkspaceModalOpen} onSuccess={handleCreateWorkspace} + enableConflictCheck /> ); From e2c980e7de42ed4b053b7741f205e64a16ee5ea7 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 22:13:59 +0900 Subject: [PATCH 36/37] Add queryInvalidation on creating workspace --- frontend/src/components/modals/CreateModal.tsx | 2 +- frontend/src/hooks/api/workspace.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/modals/CreateModal.tsx b/frontend/src/components/modals/CreateModal.tsx index bf3fd712..eb37068b 100644 --- a/frontend/src/components/modals/CreateModal.tsx +++ b/frontend/src/components/modals/CreateModal.tsx @@ -88,7 +88,7 @@ function CreateModal(props: CreateModalProps) { { }; export const useCreateWorkspaceMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: async (data: CreateWorkspaceRequest) => { const res = await axios.post("/workspaces", data); return res.data; }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: generateGetWorkspaceListQueryKey(), + }); + }, }); }; From a6d9ed8b4170da8949c2f48a63b91f6ad2230488 Mon Sep 17 00:00:00 2001 From: devleejb Date: Wed, 24 Jan 2024 22:15:29 +0900 Subject: [PATCH 37/37] Move to the note page when created --- frontend/src/components/drawers/WorkspaceDrawer.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/drawers/WorkspaceDrawer.tsx b/frontend/src/components/drawers/WorkspaceDrawer.tsx index 2655b7bd..941d7362 100644 --- a/frontend/src/components/drawers/WorkspaceDrawer.tsx +++ b/frontend/src/components/drawers/WorkspaceDrawer.tsx @@ -17,7 +17,7 @@ import { useSelector } from "react-redux"; import { selectUser } from "../../store/userSlice"; import { MouseEventHandler, useState } from "react"; import ProfilePopover from "../popovers/ProfilePopover"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { useGetWorkspaceQuery } from "../../hooks/api/workspace"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; @@ -32,6 +32,7 @@ import MemberModal from "../modals/MemberModal"; const DRAWER_WIDTH = 240; function WorkspaceDrawer() { + const navigate = useNavigate(); const params = useParams(); const userStore = useSelector(selectUser); const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug); @@ -60,7 +61,9 @@ function WorkspaceDrawer() { }; const handleCreateWorkspace = async (data: { title: string }) => { - await createDocument(data); + const document = await createDocument(data); + + navigate(document.id); }; const handleCreateWorkspaceModalOpen = () => {