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 11e4fe8fa..5bbe2ea84 100644 --- a/apps/app/components/project/issues/ListView/index.tsx +++ b/apps/app/components/project/issues/ListView/index.tsx @@ -1,9 +1,8 @@ // react -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; // next import Link from "next/link"; import Image from "next/image"; -import dynamic from "next/dynamic"; // swr import useSWR, { mutate } from "swr"; // ui @@ -11,7 +10,7 @@ import { Listbox, Transition } from "@headlessui/react"; // icons import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; // types -import { IIssue, IssueResponse, NestedKeyOf, Properties, WorkspaceMember } from "types"; +import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember } from "types"; // hooks import useUser from "lib/hooks/useUser"; // fetch keys @@ -48,7 +47,7 @@ const ListView: React.FC = ({ const [issuePreviewModal, setIssuePreviewModal] = useState(false); const [previewModalIssueId, setPreviewModalIssueId] = useState(null); - const { activeWorkspace, activeProject, states, issues } = useUser(); + const { activeWorkspace, activeProject, states } = useUser(); const partialUpdateIssue = (formData: Partial, issueId: string) => { if (!activeWorkspace || !activeProject) return; @@ -70,10 +69,6 @@ const ListView: React.FC = ({ }); }; - const LexicalViewer = dynamic(() => import("components/lexical/viewer"), { - ssr: false, - }); - const { data: people } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null @@ -179,14 +174,6 @@ const ListView: React.FC = ({ {activeProject?.identifier}-{issue.sequence_id} - ) : (key as keyof Properties) === "description" ? ( - - {/* */} - {issue.description} - ) : (key as keyof Properties) === "priority" ? ( = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - + {PRIORITIES?.map((priority) => ( = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - + {people?.map((person) => ( classNames( active ? "bg-indigo-50" : "bg-white", - "cursor-pointer select-none p-2" + "cursor-pointer select-none px-3 py-2" ) } value={person.member.id} @@ -305,15 +292,15 @@ const ListView: React.FC = ({
{person.member.avatar && person.member.avatar !== "" ? ( -
+
avatar = ({ />
) : ( - +

{person.member.first_name.charAt(0)} - +

)} - {person.member.first_name} +

{person.member.first_name}

))} @@ -375,24 +362,18 @@ const ListView: React.FC = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - + {states?.map((state) => ( classNames( active ? "bg-indigo-50" : "bg-white", - "flex items-center gap-2 cursor-pointer select-none p-2" + "cursor-pointer select-none px-3 py-2" ) } value={state.id} > - {addSpaceIfCamelCase(state.name)} ))} @@ -403,10 +384,6 @@ const ListView: React.FC = ({ )} - ) : (key as keyof Properties) === "children" ? ( - - No children. - ) : (key as keyof Properties) === "target_date" ? ( {issue.target_date @@ -463,4 +440,4 @@ const ListView: React.FC = ({ ); }; -export default ListView; +export default ListView; \ No newline at end of file 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 dcefeb086..f62b01263 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 5bf346015..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,12 +189,53 @@ 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 ? ( Active @@ -167,7 +260,17 @@ const ProjectMembers: NextPage = () => { @@ -178,20 +281,12 @@ const ProjectMembers: NextPage = () => {