From 7ad10e8e1e9cc851aab4942c1f3e1e7185ffb0d7 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Tue, 6 Dec 2022 10:11:56 +0530 Subject: [PATCH] feat: edit, delete project/workspaces member workflow, order issues by priority --- .../project/ConfirmProjectMemberRemove.tsx | 116 +++++++++++++ .../project/SendProjectInvitationModal.tsx | 11 +- .../project/issues/ListView/index.tsx | 8 - .../ConfirmWorkspaceMemberRemove.tsx | 116 +++++++++++++ apps/app/constants/index.ts | 7 + apps/app/layouts/Navbar/Sidebar.tsx | 24 ++- apps/app/lib/hooks/useIssuesProperties.tsx | 7 - apps/app/lib/services/project.service.ts | 7 +- apps/app/lib/services/workspace.service.ts | 6 +- .../projects/[projectId]/issues/index.tsx | 23 +-- .../pages/projects/[projectId]/members.tsx | 141 +++++++++++++--- .../pages/projects/[projectId]/settings.tsx | 5 + .../[invitationId].tsx | 13 +- apps/app/pages/workspace/members.tsx | 152 +++++++++++++----- apps/app/types/issues.d.ts | 7 - apps/app/ui/CustomMenu/index.tsx | 12 +- apps/app/ui/CustomMenu/types.d.ts | 1 + 17 files changed, 540 insertions(+), 116 deletions(-) create mode 100644 apps/app/components/project/ConfirmProjectMemberRemove.tsx create mode 100644 apps/app/components/workspace/ConfirmWorkspaceMemberRemove.tsx diff --git a/apps/app/components/project/ConfirmProjectMemberRemove.tsx b/apps/app/components/project/ConfirmProjectMemberRemove.tsx new file mode 100644 index 000000000..51b1d15f3 --- /dev/null +++ b/apps/app/components/project/ConfirmProjectMemberRemove.tsx @@ -0,0 +1,116 @@ +import React, { useRef, useState } from "react"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +// ui +import { Button } from "ui"; + +type Props = { + isOpen: boolean; + onClose: () => void; + handleDelete: () => void; + data?: any; +}; + +const ConfirmProjectMemberRemove: React.FC = ({ isOpen, onClose, data, handleDelete }) => { + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const cancelButtonRef = useRef(null); + + const handleClose = () => { + onClose(); + setIsDeleteLoading(false); + }; + + const handleDeletion = async () => { + setIsDeleteLoading(true); + handleDelete(); + handleClose(); + }; + + return ( + + + +
+ + +
+
+ + +
+
+
+
+
+ + Remove {data?.email}? + +
+

+ Are you sure you want to remove member - {`"`} + {data?.email} + {`"`} ? They will no longer have access to this project. This action + cannot be undone. +

+
+
+
+
+
+ + +
+
+
+
+
+
+
+ ); +}; + +export default ConfirmProjectMemberRemove; diff --git a/apps/app/components/project/SendProjectInvitationModal.tsx b/apps/app/components/project/SendProjectInvitationModal.tsx index f5658a304..a01d36658 100644 --- a/apps/app/components/project/SendProjectInvitationModal.tsx +++ b/apps/app/components/project/SendProjectInvitationModal.tsx @@ -12,6 +12,7 @@ import useToast from "lib/hooks/useToast"; import projectService from "lib/services/project.service"; import workspaceService from "lib/services/workspace.service"; // constants +import { ROLE } from "constants/"; import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // ui import { Button, Select, TextArea } from "ui"; @@ -30,13 +31,9 @@ type Props = { const defaultValues: Partial = { email: "", message: "", -}; - -const ROLE = { - 5: "Guest", - 10: "Viewer", - 15: "Member", - 20: "Admin", + role: 5, + member_id: "", + user_id: "", }; const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, members }) => { diff --git a/apps/app/components/project/issues/ListView/index.tsx b/apps/app/components/project/issues/ListView/index.tsx index 8683bfc2f..801de60ae 100644 --- a/apps/app/components/project/issues/ListView/index.tsx +++ b/apps/app/components/project/issues/ListView/index.tsx @@ -174,10 +174,6 @@ const ListView: React.FC = ({ {activeProject?.identifier}-{issue.sequence_id} - ) : (key as keyof Properties) === "description" ? ( - - {issue.description} - ) : (key as keyof Properties) === "priority" ? ( = ({ )} - ) : (key as keyof Properties) === "children" ? ( - - No children. - ) : (key as keyof Properties) === "target_date" ? ( {issue.target_date diff --git a/apps/app/components/workspace/ConfirmWorkspaceMemberRemove.tsx b/apps/app/components/workspace/ConfirmWorkspaceMemberRemove.tsx new file mode 100644 index 000000000..e2641abb4 --- /dev/null +++ b/apps/app/components/workspace/ConfirmWorkspaceMemberRemove.tsx @@ -0,0 +1,116 @@ +import React, { useRef, useState } from "react"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +// ui +import { Button } from "ui"; + +type Props = { + isOpen: boolean; + onClose: () => void; + handleDelete: () => void; + data?: any; +}; + +const ConfirmWorkspaceMemberRemove: React.FC = ({ isOpen, onClose, data, handleDelete }) => { + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const cancelButtonRef = useRef(null); + + const handleClose = () => { + onClose(); + setIsDeleteLoading(false); + }; + + const handleDeletion = async () => { + setIsDeleteLoading(true); + handleDelete(); + handleClose(); + }; + + return ( + + + +
+ + +
+
+ + +
+
+
+
+
+ + Remove {data?.email}? + +
+

+ Are you sure you want to remove member - {`"`} + {data?.email} + {`"`} ? They will no longer have access to this workspace. This action + cannot be undone. +

+
+
+
+
+
+ + +
+
+
+
+
+
+
+ ); +}; + +export default ConfirmWorkspaceMemberRemove; diff --git a/apps/app/constants/index.ts b/apps/app/constants/index.ts index 60f9994db..9e319009d 100644 --- a/apps/app/constants/index.ts +++ b/apps/app/constants/index.ts @@ -1 +1,8 @@ export const PRIORITIES = ["urgent", "high", "medium", "low"]; + +export const ROLE = { + 5: "Guest", + 10: "Viewer", + 15: "Member", + 20: "Admin", +}; diff --git a/apps/app/layouts/Navbar/Sidebar.tsx b/apps/app/layouts/Navbar/Sidebar.tsx index 48c6f7842..eb90d65bc 100644 --- a/apps/app/layouts/Navbar/Sidebar.tsx +++ b/apps/app/layouts/Navbar/Sidebar.tsx @@ -298,14 +298,22 @@ const Sidebar: React.FC = () => { ) : (

No workspace found!

)} - - {(active) => ( - - - - Create Workspace - - + { + router.push("/create-workspace"); + }} + className="w-full" + > + {({ active }) => ( + + + Create Workspace + )} diff --git a/apps/app/lib/hooks/useIssuesProperties.tsx b/apps/app/lib/hooks/useIssuesProperties.tsx index 14de06969..068af7a79 100644 --- a/apps/app/lib/hooks/useIssuesProperties.tsx +++ b/apps/app/lib/hooks/useIssuesProperties.tsx @@ -11,19 +11,12 @@ import useUser from "./useUser"; import { IssuePriorities, Properties } from "types"; const initialValues: Properties = { - name: true, key: true, - parent: false, - project: false, state: true, assignee: true, - description: false, priority: false, start_date: false, target_date: false, - sequence_id: false, - attachments: false, - children: false, cycle: false, }; diff --git a/apps/app/lib/services/project.service.ts b/apps/app/lib/services/project.service.ts index 6980a132a..107bca15f 100644 --- a/apps/app/lib/services/project.service.ts +++ b/apps/app/lib/services/project.service.ts @@ -124,12 +124,14 @@ class ProjectServices extends APIService { throw error?.response?.data; }); } + async updateProjectMember( workspace_slug: string, project_id: string, - memberId: string + memberId: string, + data: any ): Promise { - return this.put(PROJECT_MEMBER_DETAIL(workspace_slug, project_id, memberId)) + return this.put(PROJECT_MEMBER_DETAIL(workspace_slug, project_id, memberId), data) .then((response) => { return response?.data; }) @@ -137,6 +139,7 @@ class ProjectServices extends APIService { throw error?.response?.data; }); } + async deleteProjectMember( workspace_slug: string, project_id: string, diff --git a/apps/app/lib/services/workspace.service.ts b/apps/app/lib/services/workspace.service.ts index 2a3dc4788..04c158fd7 100644 --- a/apps/app/lib/services/workspace.service.ts +++ b/apps/app/lib/services/workspace.service.ts @@ -111,8 +111,9 @@ class WorkspaceService extends APIService { throw error?.response?.data; }); } - async updateWorkspaceMember(workspace_slug: string, memberId: string): Promise { - return this.put(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId)) + + async updateWorkspaceMember(workspace_slug: string, memberId: string, data: any): Promise { + return this.put(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId), data) .then((response) => { return response?.data; }) @@ -120,6 +121,7 @@ class WorkspaceService extends APIService { throw error?.response?.data; }); } + async deleteWorkspaceMember(workspace_slug: string, memberId: string): Promise { return this.delete(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId)) .then((response) => { diff --git a/apps/app/pages/projects/[projectId]/issues/index.tsx b/apps/app/pages/projects/[projectId]/issues/index.tsx index de6744121..2c0f555d4 100644 --- a/apps/app/pages/projects/[projectId]/issues/index.tsx +++ b/apps/app/pages/projects/[projectId]/issues/index.tsx @@ -36,15 +36,17 @@ import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; // types import type { IIssue, Properties, NestedKeyOf, ProjectMember } from "types"; -const groupByOptions: Array<{ name: string; key: NestedKeyOf }> = [ +const groupByOptions: Array<{ name: string; key: NestedKeyOf | null }> = [ { name: "State", key: "state_detail.name" }, { name: "Priority", key: "priority" }, { name: "Created By", key: "created_by" }, + { name: "None", key: null }, ]; const orderByOptions: Array<{ name: string; key: NestedKeyOf }> = [ { name: "Created", key: "created_at" }, { name: "Update", key: "updated_at" }, + { name: "Priority", key: "priority" }, ]; const filterIssueOptions: Array<{ @@ -222,14 +224,17 @@ const ProjectIssues: NextPage = () => { "Select" } > - {orderByOptions.map((option) => ( - setOrderBy(option.key)} - > - {option.name} - - ))} + {orderByOptions.map((option) => + groupByProperty === "priority" && + option.key === "priority" ? null : ( + setOrderBy(option.key)} + > + {option.name} + + ) + )}
diff --git a/apps/app/pages/projects/[projectId]/members.tsx b/apps/app/pages/projects/[projectId]/members.tsx index 1d049c792..5352b39eb 100644 --- a/apps/app/pages/projects/[projectId]/members.tsx +++ b/apps/app/pages/projects/[projectId]/members.tsx @@ -1,7 +1,8 @@ import React, { useState } from "react"; // next -import { useRouter } from "next/router"; +import Image from "next/image"; import type { NextPage } from "next"; +import { useRouter } from "next/router"; // swr import useSWR from "swr"; // headless ui @@ -10,19 +11,20 @@ import { Menu } from "@headlessui/react"; import projectService from "lib/services/project.service"; // hooks import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; // fetching keys import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys"; // layouts import AdminLayout from "layouts/AdminLayout"; // components import SendProjectInvitationModal from "components/project/SendProjectInvitationModal"; +import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove"; // ui -import { Spinner, Button } from "ui"; +import { Spinner, CustomListbox } from "ui"; // icons import { PlusIcon, EllipsisHorizontalIcon } from "@heroicons/react/20/solid"; import HeaderButton from "ui/HeaderButton"; import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; -import Image from "next/image"; const ROLE = { 5: "Guest", @@ -33,8 +35,16 @@ const ROLE = { const ProjectMembers: NextPage = () => { const [isOpen, setIsOpen] = useState(false); + + const [selectedMember, setSelectedMember] = useState(null); + + const [selectedRemoveMember, setSelectedRemoveMember] = useState(null); + const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState(null); + const { activeWorkspace, activeProject } = useUser(); + const { setToastAlert } = useToast(); + const router = useRouter(); const { projectId } = router.query; @@ -75,6 +85,48 @@ const ProjectMembers: NextPage = () => { return ( + { + setSelectedRemoveMember(null); + setSelectedInviteRemoveMember(null); + }} + data={members.find( + (item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember + )} + handleDelete={async () => { + if (!activeWorkspace || !projectId) return; + if (selectedRemoveMember) { + await projectService.deleteProjectMember( + activeWorkspace.slug, + projectId as string, + selectedRemoveMember + ); + mutateMembers( + (prevData: any[]) => + prevData?.filter((item: any) => item.id !== selectedRemoveMember), + false + ); + } + if (selectedInviteRemoveMember) { + await projectService.deleteProjectInvitation( + activeWorkspace.slug, + projectId as string, + selectedInviteRemoveMember + ); + mutateInvitations( + (prevData: any[]) => + prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember), + false + ); + } + setToastAlert({ + type: "success", + message: "Member removed successfully", + title: "Success", + }); + }} + /> {!projectMembers || !projectInvitations ? (
@@ -137,14 +189,55 @@ const ProjectMembers: NextPage = () => { {member.email ?? "No email has been added."} - {ROLE[member.role as keyof typeof ROLE] ?? "None"} + {selectedMember === member.id ? ( + ({ + value: key, + display: ROLE[parseInt(key) as keyof typeof ROLE], + }))} + title={ROLE[member.role as keyof typeof ROLE] ?? "Select Role"} + value={member.role} + onChange={(value) => { + if (!activeWorkspace || !projectId) return; + projectService + .updateProjectMember( + activeWorkspace.slug, + projectId as string, + member.id, + { + role: value, + } + ) + .then((res) => { + setToastAlert({ + type: "success", + message: "Member role updated successfully.", + title: "Success", + }); + mutateMembers( + (prevData: any) => + prevData.map((m: any) => { + return m.id === selectedMember + ? { ...m, ...res, role: value } + : m; + }), + false + ); + setSelectedMember(null); + }) + .catch((err) => { + console.log(err); + }); + }} + /> + ) : ( + ROLE[member.role as keyof typeof ROLE] ?? "None" + )} - {member?.member ? ( - "Member" - ) : member.status ? ( + {member.status ? ( - Accepted + Active ) : ( @@ -167,7 +260,17 @@ const ProjectMembers: NextPage = () => { @@ -178,20 +281,12 @@ const ProjectMembers: NextPage = () => {
+
+ +
diff --git a/apps/app/pages/workspace-member-invitation/[invitationId].tsx b/apps/app/pages/workspace-member-invitation/[invitationId].tsx index 483664316..9de2fb98d 100644 --- a/apps/app/pages/workspace-member-invitation/[invitationId].tsx +++ b/apps/app/pages/workspace-member-invitation/[invitationId].tsx @@ -14,7 +14,7 @@ import useUser from "lib/hooks/useUser"; // layouts import DefaultLayout from "layouts/DefaultLayout"; // ui -import { Button } from "ui"; +import { Button, Spinner } from "ui"; // icons import { ChartBarIcon, @@ -35,8 +35,9 @@ const WorkspaceInvitation: NextPage = () => { const { user } = useUser(); - const { data: invitationDetail, error } = useSWR(WORKSPACE_INVITATION, () => - workspaceService.getWorkspaceInvitation(invitationId as string) + const { data: invitationDetail, error } = useSWR( + invitationId && WORKSPACE_INVITATION, + () => invitationId && workspaceService.getWorkspaceInvitation(invitationId as string) ); const handleAccept = () => { @@ -93,7 +94,7 @@ const WorkspaceInvitation: NextPage = () => { )} - ) : ( + ) : error ? ( { }} /> + ) : ( +
+ +
)}
diff --git a/apps/app/pages/workspace/members.tsx b/apps/app/pages/workspace/members.tsx index 3ccb7d2a2..dbca98178 100644 --- a/apps/app/pages/workspace/members.tsx +++ b/apps/app/pages/workspace/members.tsx @@ -8,9 +8,11 @@ import useSWR from "swr"; import { Menu } from "@headlessui/react"; // hooks import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; // services import workspaceService from "lib/services/workspace.service"; // constants +import { ROLE } from "constants/"; import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // hoc import withAuthWrapper from "lib/hoc/withAuthWrapper"; @@ -18,26 +20,26 @@ import withAuthWrapper from "lib/hoc/withAuthWrapper"; import AdminLayout from "layouts/AdminLayout"; // components import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal"; +import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove"; // ui -import { Spinner, Button } from "ui"; +import { Spinner, CustomListbox } from "ui"; // icons import { PlusIcon, EllipsisHorizontalIcon } from "@heroicons/react/20/solid"; import HeaderButton from "ui/HeaderButton"; import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; -// types - -const ROLE = { - 5: "Guest", - 10: "Viewer", - 15: "Member", - 20: "Admin", -}; const WorkspaceInvite: NextPage = () => { const [isOpen, setIsOpen] = useState(false); + const [selectedMember, setSelectedMember] = useState(null); + + const [selectedRemoveMember, setSelectedRemoveMember] = useState(null); + const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState(null); + const { activeWorkspace } = useUser(); + const { setToastAlert } = useToast(); + const { data: workspaceMembers, mutate: mutateMembers } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null @@ -74,6 +76,50 @@ const WorkspaceInvite: NextPage = () => { title: "Plane - Workspace Invite", }} > + { + setSelectedRemoveMember(null); + setSelectedInviteRemoveMember(null); + }} + data={ + selectedRemoveMember + ? members.find((item) => item.id === selectedRemoveMember) + : selectedInviteRemoveMember + ? members.find((item) => item.id === selectedInviteRemoveMember) + : null + } + handleDelete={async () => { + if (!activeWorkspace) return; + if (selectedRemoveMember) { + await workspaceService.deleteWorkspaceMember( + activeWorkspace.slug, + selectedRemoveMember + ); + mutateMembers( + (prevData) => prevData?.filter((item) => item.id !== selectedRemoveMember), + false + ); + } + if (selectedInviteRemoveMember) { + await workspaceService.deleteWorkspaceInvitations( + activeWorkspace.slug, + selectedInviteRemoveMember + ); + mutateInvitations( + (prevData) => prevData?.filter((item) => item.id !== selectedInviteRemoveMember), + false + ); + } + setToastAlert({ + type: "success", + title: "Success", + message: "Member removed successfully", + }); + setSelectedRemoveMember(null); + setSelectedInviteRemoveMember(null); + }} + /> { {member.email ?? "No email has been added."} - {ROLE[member.role as keyof typeof ROLE] ?? "None"} + {selectedMember === member.id ? ( + ({ + display: ROLE[parseInt(key) as keyof typeof ROLE], + value: key, + }))} + title={ROLE[member.role as keyof typeof ROLE] ?? "None"} + value={member.role} + onChange={(value) => { + workspaceService + .updateWorkspaceMember(activeWorkspace?.slug as string, member.id, { + role: value, + }) + .then(() => { + mutateMembers( + (prevData) => + prevData?.map((m) => { + return m.id === selectedMember ? { ...m, role: value } : m; + }), + false + ); + setToastAlert({ + title: "Success", + type: "success", + message: "Member role updated successfully.", + }); + setSelectedMember(null); + }) + .catch(() => { + setToastAlert({ + title: "Error", + type: "error", + message: "An error occurred while updating member role.", + }); + }); + }} + /> + ) : ( + ROLE[member.role as keyof typeof ROLE] ?? "None" + )} - {member?.member ? ( + {member.status ? ( - Accepted - - ) : member.status ? ( - - Accepted + Active ) : ( @@ -173,7 +254,18 @@ const WorkspaceInvite: NextPage = () => { @@ -184,26 +276,12 @@ const WorkspaceInvite: NextPage = () => {