From aa21c97ed2729c3b59dc0c243ecc647d1e6da7ea Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Mon, 12 Dec 2022 20:14:34 +0530 Subject: [PATCH 1/2] feat: comfirm workspace deletion by typing, showing toast alert, users can't edit email from profile page feat: all added services for fetching last active workspace details --- .../project/confirm-project-deletion.tsx | 2 +- ...ion.tsx => confirm-workspace-deletion.tsx} | 83 ++++++++++++++++--- apps/app/constants/api-routes.ts | 2 +- apps/app/lib/services/workspace.service.ts | 18 +++- apps/app/pages/me/profile.tsx | 53 ++++++------ apps/app/pages/workspace/settings.tsx | 13 ++- apps/app/types/workspace.d.ts | 5 ++ 7 files changed, 128 insertions(+), 48 deletions(-) rename apps/app/components/workspace/{ConfirmWorkspaceDeletion.tsx => confirm-workspace-deletion.tsx} (60%) diff --git a/apps/app/components/project/confirm-project-deletion.tsx b/apps/app/components/project/confirm-project-deletion.tsx index 46391bb5c..159ef8db4 100644 --- a/apps/app/components/project/confirm-project-deletion.tsx +++ b/apps/app/components/project/confirm-project-deletion.tsx @@ -161,7 +161,7 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, data, onClose }) => { setConfirmDeleteMyProject(false); } }} - name="projectName" + name="typeDelete" /> diff --git a/apps/app/components/workspace/ConfirmWorkspaceDeletion.tsx b/apps/app/components/workspace/confirm-workspace-deletion.tsx similarity index 60% rename from apps/app/components/workspace/ConfirmWorkspaceDeletion.tsx rename to apps/app/components/workspace/confirm-workspace-deletion.tsx index fc6a894e4..79eeab86c 100644 --- a/apps/app/components/workspace/ConfirmWorkspaceDeletion.tsx +++ b/apps/app/components/workspace/confirm-workspace-deletion.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; // next import { useRouter } from "next/router"; // headless ui @@ -11,43 +11,54 @@ import useToast from "lib/hooks/useToast"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui -import { Button } from "ui"; +import { Button, Input } from "ui"; // types import type { IWorkspace } from "types"; type Props = { isOpen: boolean; - setIsOpen: React.Dispatch>; + data: IWorkspace | null; + onClose: () => void; }; -const ConfirmWorkspaceDeletion: React.FC = ({ isOpen, setIsOpen }) => { +const ConfirmWorkspaceDeletion: React.FC = ({ isOpen, data, onClose }) => { const router = useRouter(); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const { activeWorkspace, mutateWorkspaces } = useUser(); + const [selectedWorkspace, setSelectedWorkspace] = useState(null); + + const [confirmProjectName, setConfirmProjectName] = useState(""); + const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false); + + const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject; + + const { mutateWorkspaces } = useUser(); const { setToastAlert } = useToast(); const cancelButtonRef = useRef(null); const handleClose = () => { - setIsOpen(false); + onClose(); setIsDeleteLoading(false); }; const handleDeletion = async () => { setIsDeleteLoading(true); - if (!activeWorkspace) return; + if (!data || !canDelete) return; await workspaceService - .deleteWorkspace(activeWorkspace.slug) + .deleteWorkspace(data.slug) .then(() => { handleClose(); mutateWorkspaces((prevData) => { - return (prevData ?? []).filter( - (workspace: IWorkspace) => workspace.slug !== activeWorkspace.slug - ); + return (prevData ?? []).filter((workspace: IWorkspace) => workspace.slug !== data.slug); }, false); + setToastAlert({ + type: "success", + message: "Workspace deleted successfully", + title: "Success", + }); router.push("/"); }) .catch((error) => { @@ -56,6 +67,16 @@ const ConfirmWorkspaceDeletion: React.FC = ({ isOpen, setIsOpen }) => { }); }; + useEffect(() => { + if (data) setSelectedWorkspace(data); + else { + const timer = setTimeout(() => { + setSelectedWorkspace(null); + clearTimeout(timer); + }, 350); + } + }, [data]); + return ( = ({ isOpen, setIsOpen }) => {

Are you sure you want to delete workspace - {`"`} - {activeWorkspace?.name} + {data?.name} {`"`} ? All of the data related to the workspace will be permanently removed. This action cannot be undone.

+
+

+ Enter the workspace name{" "} + {selectedWorkspace?.name} to + continue: +

+ { + setConfirmProjectName(e.target.value); + }} + name="workspaceName" + /> +
+
+

+ To confirm, type{" "} + delete my workspace below: +

+ { + if (e.target.value === "delete my workspace") { + setConfirmDeleteMyProject(true); + } else { + setConfirmDeleteMyProject(false); + } + }} + name="typeDelete" + /> +
@@ -116,7 +173,7 @@ const ConfirmWorkspaceDeletion: React.FC = ({ isOpen, setIsOpen }) => { type="button" onClick={handleDeletion} theme="danger" - disabled={isDeleteLoading} + disabled={isDeleteLoading || !canDelete} className="inline-flex sm:ml-3" > {isDeleteLoading ? "Deleting..." : "Delete"} diff --git a/apps/app/constants/api-routes.ts b/apps/app/constants/api-routes.ts index fdf3d0bc7..6c169484b 100644 --- a/apps/app/constants/api-routes.ts +++ b/apps/app/constants/api-routes.ts @@ -24,7 +24,7 @@ export const S3_URL = `/api/file-assets/`; // LIST USER INVITATIONS ---- RESPOND INVITATIONS IN BULK export const USER_WORKSPACE_INVITATIONS = "/api/users/me/invitations/workspaces/"; export const USER_PROJECT_INVITATIONS = "/api/users/me/invitations/projects/"; - +export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "/api/users/last-visited-workspace/"; export const USER_WORKSPACE_INVITATION = (invitationId: string) => `/api/users/me/invitations/${invitationId}/`; diff --git a/apps/app/lib/services/workspace.service.ts b/apps/app/lib/services/workspace.service.ts index 5d8e041d2..a582ea9b7 100644 --- a/apps/app/lib/services/workspace.service.ts +++ b/apps/app/lib/services/workspace.service.ts @@ -11,6 +11,7 @@ import { WORKSPACE_INVITATION_DETAIL, USER_WORKSPACE_INVITATION, USER_WORKSPACE_INVITATIONS, + LAST_ACTIVE_WORKSPACE_AND_PROJECTS, } from "constants/api-routes"; // services import APIService from "lib/services/api.service"; @@ -18,7 +19,12 @@ import APIService from "lib/services/api.service"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; // types -import { IWorkspace, IWorkspaceMember, IWorkspaceMemberInvitation } from "types"; +import { + ILastActiveWorkspaceDetails, + IWorkspace, + IWorkspaceMember, + IWorkspaceMemberInvitation, +} from "types"; class WorkspaceService extends APIService { constructor() { @@ -97,6 +103,16 @@ class WorkspaceService extends APIService { }); } + async getLastActiveWorkspaceAndProjects(): Promise { + return this.get(LAST_ACTIVE_WORKSPACE_AND_PROJECTS) + .then((response) => { + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + async userWorkspaceInvitations(): Promise { return this.get(USER_WORKSPACE_INVITATIONS) .then((response) => { diff --git a/apps/app/pages/me/profile.tsx b/apps/app/pages/me/profile.tsx index b161f2696..fdf369310 100644 --- a/apps/app/pages/me/profile.tsx +++ b/apps/app/pages/me/profile.tsx @@ -1,24 +1,30 @@ import React, { useEffect, useState } from "react"; // next +import Link from "next/link"; import Image from "next/image"; import type { NextPage } from "next"; +// swr +import useSWR from "swr"; // react hook form import { useForm } from "react-hook-form"; // react dropzone -import Dropzone, { useDropzone } from "react-dropzone"; +import Dropzone from "react-dropzone"; // hooks import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; // hoc import withAuth from "lib/hoc/withAuthWrapper"; // layouts import AppLayout from "layouts/AppLayout"; +// constants +import { USER_ISSUE, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; // services import userService from "lib/services/user.service"; import fileServices from "lib/services/file.service"; +import workspaceService from "lib/services/workspace.service"; // ui import { BreadcrumbItem, Breadcrumbs, Button, Input, Spinner } from "ui"; -// types -import type { IIssue, IUser, IWorkspaceMemberInvitation } from "types"; +// icons import { ChevronRightIcon, ClipboardDocumentListIcon, @@ -28,11 +34,8 @@ import { UserPlusIcon, XMarkIcon, } from "@heroicons/react/24/outline"; -import useSWR from "swr"; -import { USER_ISSUE, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; -import useToast from "lib/hooks/useToast"; -import Link from "next/link"; -import workspaceService from "lib/services/workspace.service"; +// types +import type { IIssue, IUser } from "types"; const defaultValues: Partial = { avatar: "", @@ -51,8 +54,14 @@ const Profile: NextPage = () => { const { setToastAlert } = useToast(); const onSubmit = (formData: IUser) => { + const payload: Partial = { + id: formData.id, + first_name: formData.first_name, + last_name: formData.last_name, + avatar: formData.avatar, + }; userService - .updateUser(formData) + .updateUser(payload) .then((response) => { mutateUser(response, false); setIsEditing(false); @@ -81,9 +90,8 @@ const Profile: NextPage = () => { myProfile ? () => userService.userIssues() : null ); - const { data: invitations } = useSWR( - USER_WORKSPACE_INVITATIONS, - () => workspaceService.userWorkspaceInvitations() + const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => + workspaceService.userWorkspaceInvitations() ); useEffect(() => { @@ -128,7 +136,8 @@ const Profile: NextPage = () => { <>
-
setIsEditing((prevData) => !prevData)} > @@ -137,7 +146,7 @@ const Profile: NextPage = () => { ) : ( )} -
+
{

Email ID

- {isEditing ? ( - - ) : ( -

{myProfile.email}

- )} +

{myProfile.email}

{isEditing && ( diff --git a/apps/app/pages/workspace/settings.tsx b/apps/app/pages/workspace/settings.tsx index 6c849689c..d51f48843 100644 --- a/apps/app/pages/workspace/settings.tsx +++ b/apps/app/pages/workspace/settings.tsx @@ -3,6 +3,8 @@ import React, { useEffect, useState } from "react"; import Image from "next/image"; // react hook form import { useForm } from "react-hook-form"; +// headless ui +import { Tab } from "@headlessui/react"; // react dropzone import Dropzone from "react-dropzone"; // services @@ -17,13 +19,12 @@ import AppLayout from "layouts/AppLayout"; import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; // components -import ConfirmWorkspaceDeletion from "components/workspace/ConfirmWorkspaceDeletion"; +import ConfirmWorkspaceDeletion from "components/workspace/confirm-workspace-deletion"; // ui import { Spinner, Button, Input, Select } from "ui"; import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; // types import type { IWorkspace } from "types"; -import { Tab } from "@headlessui/react"; const defaultValues: Partial = { name: "", @@ -90,7 +91,13 @@ const WorkspaceSettings = () => { title: "Plane - Workspace Settings", }} > - + { + setIsOpen(false); + }} + data={activeWorkspace ?? null} + />
diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index 681841c32..528e3aa48 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -36,3 +36,8 @@ export interface IWorkspaceMember { created_by: string; updated_by: string; } + +export interface ILastActiveWorkspaceDetails { + workspace_details: IWorkspace; + project_details: IProject[]; +} From b540c884c5f42b370c00cbc39fea45580b28e002 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Tue, 13 Dec 2022 11:25:33 +0530 Subject: [PATCH 2/2] fix: 404 for my issues endpoint since api endpoint was changed it was causing 404 for my issues, the new endpoint sends issues isolated to the workspace --- apps/app/constants/api-routes.ts | 5 ++--- apps/app/constants/fetch-keys.ts | 2 +- apps/app/lib/services/user.service.ts | 4 ++-- apps/app/pages/me/my-issues.tsx | 6 +++--- apps/app/pages/me/profile.tsx | 8 ++++---- apps/app/pages/workspace/index.tsx | 4 ++-- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/app/constants/api-routes.ts b/apps/app/constants/api-routes.ts index 6c169484b..aa2fd82d6 100644 --- a/apps/app/constants/api-routes.ts +++ b/apps/app/constants/api-routes.ts @@ -15,7 +15,8 @@ export const MAGIC_LINK_SIGNIN = "/api/magic-sign-in/"; export const USER_ENDPOINT = "/api/users/me/"; export const CHANGE_PASSWORD = "/api/users/me/change-password/"; export const USER_ONBOARD_ENDPOINT = "/api/users/me/onboard/"; -export const USER_ISSUES_ENDPOINT = "/api/users/me/issues/"; +export const USER_ISSUES_ENDPOINT = (workspaceSlug: string) => + `/api/workspaces/${workspaceSlug}/my-issues/`; export const USER_WORKSPACES = "/api/users/me/workspaces"; // s3 file url @@ -33,8 +34,6 @@ export const JOIN_WORKSPACE = (workspaceSlug: string, invitationId: string) => export const JOIN_PROJECT = (workspaceSlug: string) => `/api/workspaces/${workspaceSlug}/projects/join/`; -export const USER_ISSUES = "/api/users/me/issues/"; - // workspaces export const WORKSPACES_ENDPOINT = "/api/workspaces/"; export const WORKSPACE_DETAIL = (workspaceSlug: string) => `/api/workspaces/${workspaceSlug}/`; diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 9e3611024..76a3807ae 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -29,4 +29,4 @@ export const CYCLE_DETAIL = "CYCLE_DETAIL"; export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`; export const STATE_DETAIL = "STATE_DETAIL"; -export const USER_ISSUE = "USER_ISSUE"; +export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug}`; diff --git a/apps/app/lib/services/user.service.ts b/apps/app/lib/services/user.service.ts index 8266a617c..e4fe684fd 100644 --- a/apps/app/lib/services/user.service.ts +++ b/apps/app/lib/services/user.service.ts @@ -16,8 +16,8 @@ class UserService extends APIService { }; } - async userIssues(): Promise { - return this.get(USER_ISSUES_ENDPOINT) + async userIssues(workspaceSlug: string): Promise { + return this.get(USER_ISSUES_ENDPOINT(workspaceSlug)) .then((response) => { return response?.data; }) diff --git a/apps/app/pages/me/my-issues.tsx b/apps/app/pages/me/my-issues.tsx index 444b0f342..d98c63499 100644 --- a/apps/app/pages/me/my-issues.tsx +++ b/apps/app/pages/me/my-issues.tsx @@ -33,11 +33,11 @@ import { Menu, Transition } from "@headlessui/react"; const MyIssues: NextPage = () => { const [selectedWorkspace, setSelectedWorkspace] = useState(null); - const { user, workspaces } = useUser(); + const { user, workspaces, activeWorkspace } = useUser(); const { data: myIssues, mutate: mutateMyIssues } = useSWR( - user ? USER_ISSUE : null, - user ? () => userService.userIssues() : null + user && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null, + user && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null ); const updateMyIssues = ( diff --git a/apps/app/pages/me/profile.tsx b/apps/app/pages/me/profile.tsx index fdf369310..b256b141d 100644 --- a/apps/app/pages/me/profile.tsx +++ b/apps/app/pages/me/profile.tsx @@ -49,7 +49,7 @@ const Profile: NextPage = () => { const [isImageUploading, setIsImageUploading] = useState(false); const [isEditing, setIsEditing] = useState(false); - const { user: myProfile, mutateUser, projects } = useUser(); + const { user: myProfile, mutateUser, projects, activeWorkspace } = useUser(); const { setToastAlert } = useToast(); @@ -86,8 +86,8 @@ const Profile: NextPage = () => { } = useForm({ defaultValues }); const { data: myIssues } = useSWR( - myProfile ? USER_ISSUE : null, - myProfile ? () => userService.userIssues() : null + myProfile && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null, + myProfile && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null ); const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => @@ -103,7 +103,7 @@ const Profile: NextPage = () => { icon: RectangleStackIcon, title: "My Issues", number: myIssues?.length ?? 0, - description: "View the list of issues assigned to you across the workspace.", + description: "View the list of issues assigned to you for this workspace.", href: "/me/my-issues", }, { diff --git a/apps/app/pages/workspace/index.tsx b/apps/app/pages/workspace/index.tsx index 26779bd9b..578c3f4db 100644 --- a/apps/app/pages/workspace/index.tsx +++ b/apps/app/pages/workspace/index.tsx @@ -26,8 +26,8 @@ const Workspace: NextPage = () => { const { user, activeWorkspace, projects } = useUser(); const { data: myIssues } = useSWR( - user ? USER_ISSUE : null, - user ? () => userService.userIssues() : null + user && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null, + user && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null ); const cards = [