From a7d6b528bd3180abc0566e3d76f013ecf12d2fc3 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:44:06 +0530 Subject: [PATCH] chore: deactivate user option added (#2841) * dev: deactivate user option added * chore: new layout for profile settings * fix: build errors * fix: user profile activity --- ...modal.tsx => deactivate-account-modal.tsx} | 93 ++++++------ web/components/account/index.ts | 1 + .../auth-screens/project/join-project.tsx | 6 +- .../command-palette/command-pallette.tsx | 14 +- web/components/core/activity.tsx | 97 ++++++------ web/components/headers/index.ts | 1 + web/components/headers/profile-settings.tsx | 30 ++++ web/components/headers/user-profile.tsx | 40 ++--- web/components/instance/sidebar-dropdown.tsx | 2 +- web/components/issues/delete-issue-modal.tsx | 4 +- web/components/profile/sidebar.tsx | 2 +- .../project/confirm-project-member-remove.tsx | 37 +++-- web/components/project/join-project-modal.tsx | 2 +- .../project/leave-project-modal.tsx | 7 +- web/components/project/member-list-item.tsx | 141 +++++++----------- web/components/project/member-list.tsx | 64 ++------ .../project/send-project-invitation-modal.tsx | 49 +++--- web/components/project/sidebar-list.tsx | 29 ++-- .../confirm-workspace-member-remove.tsx | 18 ++- web/components/workspace/help-section.tsx | 14 +- .../workspace/settings/members-list-item.tsx | 103 ++++++++----- .../workspace/settings/members-list.tsx | 8 +- web/components/workspace/sidebar-dropdown.tsx | 6 +- web/layouts/admin-layout/sidebar.tsx | 1 - web/layouts/app-layout/sidebar.tsx | 1 - web/layouts/settings-layout/index.ts | 1 + web/layouts/settings-layout/profile/index.ts | 3 + .../settings-layout/profile/layout.tsx | 35 +++++ .../profile/settings-sidebar.tsx | 48 ++++++ .../settings-layout/profile/sidebar.tsx | 119 +++++++++++++++ .../project/{index.tsx => index.ts} | 0 .../workspace/{index.tsx => index.ts} | 0 .../settings-layout/workspace/sidebar.tsx | 45 ------ .../index.ts | 0 .../layout.tsx | 0 .../profile/[userId]/assigned.tsx | 4 +- .../profile/[userId]/created.tsx | 4 +- .../profile/[userId]/index.tsx | 4 +- .../profile/[userId]/subscribed.tsx | 4 +- .../[workspaceSlug]/settings/members.tsx | 15 +- .../me/profile/activity.tsx | 16 +- .../me/profile/index.tsx | 105 ++++++++----- .../me/profile/preferences.tsx | 11 +- web/pages/onboarding/index.tsx | 11 +- .../project/project-member.service.ts | 24 --- web/services/project/project.service.ts | 16 -- web/services/user.service.ts | 33 +++- web/store/project/project-members.store.ts | 13 +- web/store/project/project.store.ts | 54 ------- web/store/user.store.ts | 74 ++++++++- web/store/workspace/workspace-member.store.ts | 12 +- web/types/issues.d.ts | 1 + web/types/users.d.ts | 2 +- 53 files changed, 799 insertions(+), 625 deletions(-) rename web/components/account/{delete-account-modal.tsx => deactivate-account-modal.tsx} (67%) create mode 100644 web/components/headers/profile-settings.tsx create mode 100644 web/layouts/settings-layout/profile/index.ts create mode 100644 web/layouts/settings-layout/profile/layout.tsx create mode 100644 web/layouts/settings-layout/profile/settings-sidebar.tsx create mode 100644 web/layouts/settings-layout/profile/sidebar.tsx rename web/layouts/settings-layout/project/{index.tsx => index.ts} (100%) rename web/layouts/settings-layout/workspace/{index.tsx => index.ts} (100%) rename web/layouts/{profile-layout => user-profile-layout}/index.ts (100%) rename web/layouts/{profile-layout => user-profile-layout}/layout.tsx (100%) rename web/pages/{[workspaceSlug] => }/me/profile/activity.tsx (94%) rename web/pages/{[workspaceSlug] => }/me/profile/index.tsx (79%) rename web/pages/{[workspaceSlug] => }/me/profile/preferences.tsx (87%) diff --git a/web/components/account/delete-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx similarity index 67% rename from web/components/account/delete-account-modal.tsx rename to web/components/account/deactivate-account-modal.tsx index 41533d69c..22a5d5b89 100644 --- a/web/components/account/delete-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -1,18 +1,17 @@ -// react import React, { useState } from "react"; -// next import { useRouter } from "next/router"; +import { mutate } from "swr"; +import { useTheme } from "next-themes"; +import { Dialog, Transition } from "@headlessui/react"; +import { AlertTriangle } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { Button } from "@plane/ui"; // hooks import useToast from "hooks/use-toast"; // services import { AuthService } from "services/auth.service"; -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// icons -import { Trash2 } from "lucide-react"; -import { UserService } from "services/user.service"; -import { useTheme } from "next-themes"; -import { mutate } from "swr"; type Props = { isOpen: boolean; @@ -20,23 +19,40 @@ type Props = { }; const authService = new AuthService(); -const userService = new UserService(); -const DeleteAccountModal: React.FC = (props) => { +export const DeactivateAccountModal: React.FC = (props) => { const { isOpen, onClose } = props; - const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + // states + const [switchingAccount, setSwitchingAccount] = useState(false); + const [isDeactivating, setIsDeactivating] = useState(false); + + const { + user: { deactivateAccount }, + } = useMobxStore(); const router = useRouter(); + const { setTheme } = useTheme(); + const { setToastAlert } = useToast(); - const handleSignOut = async () => { + const handleClose = () => { + setSwitchingAccount(false); + setIsDeactivating(false); + onClose(); + }; + + const handleSwitchAccount = async () => { + setSwitchingAccount(true); + await authService .signOut() .then(() => { mutate("CURRENT_USER_DETAILS", null); setTheme("system"); router.push("/"); + handleClose(); }) .catch(() => setToastAlert({ @@ -44,35 +60,31 @@ const DeleteAccountModal: React.FC = (props) => { title: "Error!", message: "Failed to sign out. Please try again.", }) - ); + ) + .finally(() => setSwitchingAccount(false)); }; const handleDeleteAccount = async () => { - setIsDeleteLoading(true); - await userService - .deleteAccount() + setIsDeactivating(true); + + await deactivateAccount() .then(() => { setToastAlert({ type: "success", title: "Success!", message: "Account deleted successfully.", }); - mutate("CURRENT_USER_DETAILS", null); - setTheme("system"); + handleClose(); router.push("/"); }) .catch((err) => setToastAlert({ type: "error", title: "Error!", - message: err?.data?.error, + message: err?.error, }) - ); - setIsDeleteLoading(false); - }; - - const handleClose = () => { - onClose(); + ) + .finally(() => setIsDeactivating(false)); }; return ( @@ -105,32 +117,29 @@ const DeleteAccountModal: React.FC = (props) => {
-
-
    -
  • Delete this account if you have another and won’t use this account.
  • -
  • Switch to another account if you’d like to come back to this account another time.
  • +
  • Deactivate this account if you have another and won{"'"}t use this account.
  • +
  • Switch to another account if you{"'"}d like to come back to this account another time.
-
- - Switch account - - +
+ +
@@ -140,5 +149,3 @@ const DeleteAccountModal: React.FC = (props) => { ); }; - -export default DeleteAccountModal; diff --git a/web/components/account/index.ts b/web/components/account/index.ts index 36a204c62..4633d91b3 100644 --- a/web/components/account/index.ts +++ b/web/components/account/index.ts @@ -1,3 +1,4 @@ +export * from "./deactivate-account-modal"; export * from "./email-code-form"; export * from "./email-password-form"; export * from "./email-forgot-password-form"; diff --git a/web/components/auth-screens/project/join-project.tsx b/web/components/auth-screens/project/join-project.tsx index 5713e2ad8..d5841e43b 100644 --- a/web/components/auth-screens/project/join-project.tsx +++ b/web/components/auth-screens/project/join-project.tsx @@ -13,7 +13,9 @@ import JoinProjectImg from "public/auth/project-not-authorized.svg"; export const JoinProject: React.FC = () => { const [isJoiningProject, setIsJoiningProject] = useState(false); - const { project: projectStore } = useMobxStore(); + const { + user: { joinProject }, + } = useMobxStore(); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -23,7 +25,7 @@ export const JoinProject: React.FC = () => { setIsJoiningProject(true); - projectStore.joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => { + joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => { setIsJoiningProject(false); }); }; diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index 7708ff926..7530a15a6 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -4,7 +4,6 @@ import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks import useToast from "hooks/use-toast"; -import useUser from "hooks/use-user"; // components import { CommandModal, ShortcutsModal } from "components/command-palette"; import { BulkDeleteIssuesModal } from "components/core"; @@ -30,7 +29,11 @@ export const CommandPalette: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; // store - const { commandPalette, theme: themeStore } = useMobxStore(); + const { + commandPalette, + theme: { toggleSidebar }, + user: { currentUser }, + } = useMobxStore(); const { toggleCommandPaletteModal, isCreateIssueModalOpen, @@ -52,9 +55,6 @@ export const CommandPalette: FC = observer(() => { isDeleteIssueModalOpen, toggleDeleteIssueModal, } = commandPalette; - const { toggleSidebar } = themeStore; - - const { user } = useUser(); const { setToastAlert } = useToast(); @@ -153,7 +153,7 @@ export const CommandPalette: FC = observer(() => { return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); - if (!user) return null; + if (!currentUser) return null; return ( <> @@ -223,7 +223,7 @@ export const CommandPalette: FC = observer(() => { onClose={() => { toggleBulkDeleteIssueModal(false); }} - user={user} + user={currentUser} /> diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 6bddae940..fb2df6496 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -1,11 +1,9 @@ import { useRouter } from "next/router"; - -import useSWR from "swr"; - +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hook import useEstimateOption from "hooks/use-estimate-option"; -// services -import { IssueLabelService } from "services/issue"; // icons import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; import { @@ -29,11 +27,7 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // types import { IIssueActivity } from "types"; -// fetch-keys -import { WORKSPACE_LABELS } from "constants/fetch-keys"; - -// services -const issueLabelService = new IssueLabelService(); +import { useEffect } from "react"; const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const router = useRouter(); @@ -44,7 +38,11 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => { { return ( { ); }; -const LabelPill = ({ labelId }: { labelId: string }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; +const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => { + const { + workspace: { labels, fetchWorkspaceLabels }, + } = useMobxStore(); - const { data: labels } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) : null - ); + const workspaceLabels = labels[workspaceSlug]; + + useEffect(() => { + if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug); + }, [fetchWorkspaceLabels, workspaceLabels, workspaceSlug]); return ( l.id === labelId)?.color ?? "#000000", + backgroundColor: workspaceLabels?.find((l) => l.id === labelId)?.color ?? "#000000", }} aria-hidden="true" /> ); -}; +}); const EstimatePoint = ({ point }: { point: string }) => { const { estimateValue, isEstimateActive } = useEstimateOption(Number(point)); @@ -243,24 +245,6 @@ const activityDetails: { }, icon: , }, - relates_to: { - message: (activity) => { - if (activity.old_value === "") - return ( - <> - marked that this issue relates to{" "} - {activity.new_value}. - - ); - else - return ( - <> - removed the relation from {activity.old_value}. - - ); - }, - icon: , - }, cycles: { message: (activity, showIssue, workspaceSlug) => { if (activity.verb === "created") @@ -365,13 +349,13 @@ const activityDetails: { icon:
-
-
- - } - link={`/${workspaceSlug}/me/profile`} - /> - - -
+export const UserProfileHeader = () => ( +
+
+
+ + +
- ); -}); +
+); diff --git a/web/components/instance/sidebar-dropdown.tsx b/web/components/instance/sidebar-dropdown.tsx index 8989dc698..c7d449a62 100644 --- a/web/components/instance/sidebar-dropdown.tsx +++ b/web/components/instance/sidebar-dropdown.tsx @@ -25,7 +25,7 @@ const profileLinks = (workspaceSlug: string, userId: string) => [ { name: "Settings", icon: Settings, - link: `/${workspaceSlug}/me/profile`, + link: `/me/profile`, }, ]; diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 38d0a33d1..fd9e32f6d 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -75,9 +75,9 @@ export const DeleteIssueModal: React.FC = observer((props) => {
- +

Delete Issue

diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 9d2b7126c..c3a63dd67 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -67,7 +67,7 @@ export const ProfileSidebar = () => {
{user?.id === userId && (
- + diff --git a/web/components/project/confirm-project-member-remove.tsx b/web/components/project/confirm-project-member-remove.tsx index c084407a2..dff0eacd9 100644 --- a/web/components/project/confirm-project-member-remove.tsx +++ b/web/components/project/confirm-project-member-remove.tsx @@ -1,23 +1,32 @@ import React, { useState } from "react"; -// headless ui import { Dialog, Transition } from "@headlessui/react"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // icons import { AlertTriangle } from "lucide-react"; // ui import { Button } from "@plane/ui"; +// types +import { IUserLite } from "types"; type Props = { + data: IUserLite; + onSubmit: () => Promise; isOpen: boolean; onClose: () => void; - handleDelete: () => void; - data?: any; }; -export const ConfirmProjectMemberRemove: React.FC = (props) => { - const { isOpen, onClose, data, handleDelete } = props; +export const ConfirmProjectMemberRemove: React.FC = observer((props) => { + const { data, onSubmit, isOpen, onClose } = props; + // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const { + user: { currentUser }, + } = useMobxStore(); + const handleClose = () => { onClose(); setIsDeleteLoading(false); @@ -25,10 +34,14 @@ export const ConfirmProjectMemberRemove: React.FC = (props) => { const handleDeletion = async () => { setIsDeleteLoading(true); - handleDelete(); + + await onSubmit(); + handleClose(); }; + const isCurrentUser = currentUser?.id === data?.id; + return ( @@ -63,7 +76,7 @@ export const ConfirmProjectMemberRemove: React.FC = (props) => {
- Remove {data?.display_name}? + {isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}

@@ -80,7 +93,13 @@ export const ConfirmProjectMemberRemove: React.FC = (props) => { Cancel

@@ -90,4 +109,4 @@ export const ConfirmProjectMemberRemove: React.FC = (props) => { ); -}; +}); diff --git a/web/components/project/join-project-modal.tsx b/web/components/project/join-project-modal.tsx index 67adc881d..8a4e6304d 100644 --- a/web/components/project/join-project-modal.tsx +++ b/web/components/project/join-project-modal.tsx @@ -22,7 +22,7 @@ export const JoinProjectModal: React.FC = (props) => { const [isJoiningLoading, setIsJoiningLoading] = useState(false); // store const { - project: { joinProject }, + user: { joinProject }, } = useMobxStore(); // router const router = useRouter(); diff --git a/web/components/project/leave-project-modal.tsx b/web/components/project/leave-project-modal.tsx index 421234ab6..503c1df74 100644 --- a/web/components/project/leave-project-modal.tsx +++ b/web/components/project/leave-project-modal.tsx @@ -35,7 +35,9 @@ export const LeaveProjectModal: FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { project: projectStore } = useMobxStore(); + const { + user: { leaveProject }, + } = useMobxStore(); // toast const { setToastAlert } = useToast(); @@ -57,8 +59,7 @@ export const LeaveProjectModal: FC = observer((props) => { if (data) { if (data.projectName === project?.name) { if (data.confirmLeave === "Leave Project") { - return projectStore - .leaveProject(workspaceSlug.toString(), project.id) + return leaveProject(workspaceSlug.toString(), project.id) .then(() => { handleClose(); router.push(`/${workspaceSlug}/projects`); diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 308ca8827..df60c75ec 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; -import useSWR, { mutate } from "swr"; import { observer } from "mobx-react-lite"; +// mobx store import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; @@ -15,113 +15,95 @@ import { ChevronDown, Dot, XCircle } from "lucide-react"; // constants import { ROLE } from "constants/workspace"; // types -import { TUserProjectRole } from "types"; +import { IProjectMember, TUserProjectRole } from "types"; type Props = { - member: any; + member: IProjectMember; }; export const ProjectMemberListItem: React.FC = observer((props) => { const { member } = props; + // states + const [removeMemberModal, setRemoveMemberModal] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // states - const [selectedRemoveMember, setSelectedRemoveMember] = useState(null); - const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState(null); + // store const { - user: userStore, - projectMember: { - projectMembers, - fetchProjectMembers, - removeMemberFromProject, - updateMember, - deleteProjectInvitation, - }, + user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject }, + projectMember: { removeMemberFromProject, updateMember }, } = useMobxStore(); // hooks const { setToastAlert } = useToast(); - // fetching project members - useSWR( - workspaceSlug && projectId ? `PROJECT_MEMBERS_${projectId.toString().toUpperCase()}` : null, - workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null - ); + // derived values - const user = userStore.currentUser; - const { currentProjectMemberInfo, currentProjectRole } = userStore; const isAdmin = currentProjectRole === 20; - const currentUser = projectMembers?.find((item) => item.member.id === user?.id); + const memberDetails = member.member; + + const handleRemove = async () => { + if (!workspaceSlug || !projectId) return; + + if (memberDetails.id === currentUser?.id) { + await leaveProject(workspaceSlug.toString(), projectId.toString()) + .then(() => router.push(`/${workspaceSlug}/projects`)) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error", + message: err?.error || "Something went wrong. Please try again.", + }) + ); + } else + await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), member.id).catch((err) => + setToastAlert({ + type: "error", + title: "Error", + message: err?.error || "Something went wrong. Please try again.", + }) + ); + }; return ( <> { - setSelectedRemoveMember(null); - setSelectedInviteRemoveMember(null); - }} - data={selectedRemoveMember ?? selectedInviteRemoveMember} - handleDelete={async () => { - if (!workspaceSlug || !projectId) return; - - // if the user is a member - if (selectedRemoveMember) { - await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), selectedRemoveMember.id); - } - // if the user is an invite - if (selectedInviteRemoveMember) { - await deleteProjectInvitation( - workspaceSlug.toString(), - projectId.toString(), - selectedInviteRemoveMember.id - ); - mutate(`PROJECT_INVITATIONS_${projectId.toString()}`); - } - - setToastAlert({ - type: "success", - message: "Member removed successfully", - title: "Success", - }); - }} + isOpen={removeMemberModal} + onClose={() => setRemoveMemberModal(false)} + data={member.member} + onSubmit={handleRemove} />
- {member.avatar && member.avatar !== "" ? ( - + {memberDetails.avatar && memberDetails.avatar !== "" ? ( + {member.display_name ) : ( - + - {(member.display_name ?? member.email ?? "?")[0]} + {(memberDetails.display_name ?? memberDetails.email ?? "?")[0]} )}
- {member.member ? ( - - - {member.first_name} {member.last_name} - - - ) : ( -

{member.display_name || member.email}

- )} + + + {memberDetails.first_name} {memberDetails.last_name} + +
-

{member.display_name}

+

{memberDetails.display_name}

{isAdmin && ( <> -

{member.email}

+

{memberDetails.email}

)}
@@ -129,23 +111,17 @@ export const ProjectMemberListItem: React.FC = observer((props) => {
- {!member?.status && ( -
-

Pending

-
- )} - {ROLE[member.role as keyof typeof ROLE]} - {member.memberId !== currentProjectMemberInfo?.id && ( + {memberDetails.id !== currentProjectMemberInfo?.id && ( @@ -170,9 +146,9 @@ export const ProjectMemberListItem: React.FC = observer((props) => { }); }} disabled={ - member.memberId === user?.id || + memberDetails.id === currentUser?.id || !member.member || - (currentUser && currentUser.role !== 20 && currentUser.role < member.role) + (currentProjectRole && currentProjectRole !== 20 && currentProjectRole < member.role) } placement="bottom-end" > @@ -188,14 +164,13 @@ export const ProjectMemberListItem: React.FC = observer((props) => { {isAdmin && (
- {!projectMembers || !projectInvitations ? ( + {!projectMembers ? ( @@ -113,7 +69,7 @@ export const ProjectMemberList: React.FC = observer(() => { ) : (
- {members.length > 0 + {projectMembers.length > 0 ? searchedMembers.map((member) => ) : null} {searchedMembers.length === 0 && ( diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index 6509a4e4f..cfcc25866 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { ChevronDown, Plus, X } from "lucide-react"; @@ -11,22 +10,19 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui"; // services import { ProjectMemberService } from "services/project"; -import { WorkspaceService } from "services/workspace.service"; // hooks import useToast from "hooks/use-toast"; +// helpers +import { trackEvent } from "helpers/event-tracker.helper"; // types -import { IUser, TUserProjectRole } from "types"; -// fetch-keys -import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; +import { IProjectMember, TUserProjectRole } from "types"; // constants import { ROLE } from "constants/workspace"; -import { trackEvent } from "helpers/event-tracker.helper"; type Props = { isOpen: boolean; - setIsOpen: React.Dispatch>; - members: any[]; - user: IUser | undefined; + members: IProjectMember[]; + onClose: () => void; onSuccess: () => void; }; @@ -50,23 +46,19 @@ const defaultValues: FormValues = { // services const projectMemberService = new ProjectMemberService(); -const workspaceService = new WorkspaceService(); export const SendProjectInvitationModal: React.FC = observer((props) => { - const { isOpen, setIsOpen, members, onSuccess } = props; + const { isOpen, members, onClose, onSuccess } = props; const router = useRouter(); const { workspaceSlug, projectId } = router.query; const { setToastAlert } = useToast(); - const { user: userStore } = useMobxStore(); - const userRole = userStore.currentProjectRole; - - const { data: people } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null, - workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug as string) : null - ); + const { + user: { currentProjectRole }, + workspaceMember: { workspaceMembers }, + } = useMobxStore(); const { formState: { errors, isSubmitting }, @@ -80,8 +72,8 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { name: "members", }); - const uninvitedPeople = people?.filter((person) => { - const isInvited = members?.find((member) => member.memberId === person.member.id); + const uninvitedPeople = workspaceMembers?.filter((person) => { + const isInvited = members?.find((member) => member.member.id === person.member.id); return !isInvited; }); @@ -93,17 +85,15 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { await projectMemberService .bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload) - .then((res) => { - setIsOpen(false); - trackEvent( - 'PROJECT_MEMBER_INVITE', - ) + .then(() => { + onSuccess(); + onClose(); + trackEvent("PROJECT_MEMBER_INVITE"); setToastAlert({ title: "Success", type: "success", message: "Member added successfully", }); - onSuccess(); }) .catch((error) => { console.log(error); @@ -114,7 +104,8 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { }; const handleClose = () => { - setIsOpen(false); + onClose(); + const timeout = setTimeout(() => { reset(defaultValues); clearTimeout(timeout); @@ -195,7 +186,7 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { name={`members.${index}.member_id`} rules={{ required: "Please select a member" }} render={({ field: { value, onChange } }) => { - const selectedMember = people?.find((p) => p.member.id === value)?.member; + const selectedMember = workspaceMembers?.find((p) => p.member.id === value)?.member; return ( = observer((props) => { width="w-full" > {Object.entries(ROLE).map(([key, label]) => { - if (parseInt(key) > (userRole ?? 5)) return null; + if (parseInt(key) > (currentProjectRole ?? 5)) return null; return ( diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index a889e7fae..b32fb4f42 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -26,7 +26,11 @@ export const ProjectSidebarList: FC = observer(() => { // refs const containerRef = useRef(null); - const { theme: themeStore, project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); + const { + theme: { sidebarCollapsed }, + project: { joinedProjects, favoriteProjects, orderProjectsWithSortOrder, updateProjectView }, + commandPalette: { toggleCreateProjectModal }, + } = useMobxStore(); // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -34,9 +38,6 @@ export const ProjectSidebarList: FC = observer(() => { // toast const { setToastAlert } = useToast(); - const joinedProjects = workspaceSlug && projectStore.joinedProjects; - const favoriteProjects = workspaceSlug && projectStore.favoriteProjects; - const orderedJoinedProjects: IProject[] | undefined = joinedProjects ? orderArrayBy(joinedProjects, "sort_order", "ascending") : undefined; @@ -62,20 +63,18 @@ export const ProjectSidebarList: FC = observer(() => { if (source.index === destination.index) return; - const updatedSortOrder = projectStore.orderProjectsWithSortOrder(source.index, destination.index, draggableId); + const updatedSortOrder = orderProjectsWithSortOrder(source.index, destination.index, draggableId); - projectStore - .updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Something went wrong. Please try again.", - }); + updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again.", }); + }); }; - const isCollapsed = themeStore.sidebarCollapsed || false; + const isCollapsed = sidebarCollapsed || false; /** * Implementing scroll animation styles based on the scroll length of the container @@ -263,7 +262,7 @@ export const ProjectSidebarList: FC = observer(() => {
diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index bdbba0de7..f38c447b1 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -2,7 +2,6 @@ import React, { useRef, useState } from "react"; import Link from "next/link"; import { Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // hooks @@ -43,7 +42,10 @@ export interface WorkspaceHelpSectionProps { export const WorkspaceHelpSection: React.FC = observer(() => { // store - const { theme: themeStore, commandPalette: commandPaletteStore } = useMobxStore(); + const { + theme: { sidebarCollapsed, toggleSidebar }, + commandPalette: { toggleShortcutModal }, + } = useMobxStore(); // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs @@ -51,7 +53,7 @@ export const WorkspaceHelpSection: React.FC = observe useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false)); - const isCollapsed = themeStore.sidebarCollapsed || false; + const isCollapsed = sidebarCollapsed || false; return ( <> @@ -71,7 +73,7 @@ export const WorkspaceHelpSection: React.FC = observe className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${ isCollapsed ? "w-full" : "" }`} - onClick={() => commandPaletteStore.toggleShortcutModal(true)} + onClick={() => toggleShortcutModal(true)} > @@ -87,7 +89,7 @@ export const WorkspaceHelpSection: React.FC = observe @@ -96,7 +98,7 @@ export const WorkspaceHelpSection: React.FC = observe className={`hidden md:grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${ isCollapsed ? "w-full" : "" }`} - onClick={() => themeStore.toggleSidebar()} + onClick={() => toggleSidebar()} > diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index 7c00f4f12..5384977f8 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -12,9 +12,10 @@ import { ConfirmWorkspaceMemberRemove } from "components/workspace"; import { CustomSelect, Tooltip } from "@plane/ui"; // icons import { ChevronDown, Dot, XCircle } from "lucide-react"; +// types +import { TUserWorkspaceRole } from "types"; // constants import { ROLE } from "constants/workspace"; -import { TUserWorkspaceRole } from "types"; type Props = { member: { @@ -40,7 +41,7 @@ export const WorkspaceMembersListItem: FC = (props) => { // store const { workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation }, - user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings }, + user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace }, } = useMobxStore(); const isAdmin = currentWorkspaceRole === 20; // states @@ -48,49 +49,69 @@ export const WorkspaceMembersListItem: FC = (props) => { // hooks const { setToastAlert } = useToast(); + const handleLeaveWorkspace = async () => { + if (!workspaceSlug || !currentUserSettings) return; + + await leaveWorkspace(workspaceSlug.toString()) + .then(() => { + if (currentUserSettings.workspace?.invites > 0) router.push("/invitations"); + else router.push("/create-workspace"); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error", + message: err?.error || "Something went wrong. Please try again.", + }) + ); + }; + const handleRemoveMember = async () => { if (!workspaceSlug) return; - if (member.member) - await removeMember(workspaceSlug.toString(), member.id) - .then(() => { - const memberId = member.memberId; + await removeMember(workspaceSlug.toString(), member.id).catch((err) => + setToastAlert({ + type: "error", + title: "Error", + message: err?.error || "Something went wrong. Please try again.", + }) + ); + }; - if (memberId === currentUser?.id && currentUserSettings) { - if (currentUserSettings.workspace?.invites > 0) router.push("/invitations"); - else router.push("/create-workspace"); - } - }) - .catch((err) => { - setToastAlert({ - type: "error", - title: "Error", - message: err?.error || "Something went wrong", - }); - }); - else - await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id) - .then(() => { - setToastAlert({ - type: "success", - title: "Success", - message: "Member removed successfully", - }); - }) - .catch((err) => { - setToastAlert({ - type: "error", - title: "Error", - message: err?.error || "Something went wrong", - }); - }) - .finally(() => { - mutate(`WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`, (prevData: any) => { - if (!prevData) return prevData; + const handleRemoveInvitation = async () => { + if (!workspaceSlug) return; - return prevData.filter((item: any) => item.id !== member.id); - }); - }); + await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id) + .then(() => + setToastAlert({ + type: "success", + title: "Success", + message: "Invitation removed successfully.", + }) + ) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error", + message: err?.error || "Something went wrong. Please try again.", + }) + ) + .finally(() => + mutate(`WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`, (prevData: any) => { + if (!prevData) return prevData; + + return prevData.filter((item: any) => item.id !== member.id); + }) + ); + }; + + const handleRemove = async () => { + if (member.member) { + const memberId = member.memberId; + + if (memberId === currentUser?.id) await handleLeaveWorkspace(); + else await handleRemoveMember(); + } else await handleRemoveInvitation(); }; if (!currentWorkspaceMemberInfo) return null; @@ -101,7 +122,7 @@ export const WorkspaceMembersListItem: FC = (props) => { isOpen={removeMemberModal} onClose={() => setRemoveMemberModal(false)} data={member} - onSubmit={handleRemoveMember} + onSubmit={handleRemove} />
diff --git a/web/components/workspace/settings/members-list.tsx b/web/components/workspace/settings/members-list.tsx index 2244c5cad..854c3b069 100644 --- a/web/components/workspace/settings/members-list.tsx +++ b/web/components/workspace/settings/members-list.tsx @@ -33,11 +33,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea const displayName = member.display_name.toLowerCase(); const fullName = `${member.first_name} ${member.last_name}`.toLowerCase(); - return ( - displayName.includes(searchQuery.toLowerCase()) || - fullName.includes(searchQuery.toLowerCase()) || - email?.includes(searchQuery.toLowerCase()) - ); + return `${email}${displayName}${fullName}`.includes(searchQuery.toLowerCase()); }); if ( @@ -61,7 +57,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea ? searchedMembers?.map((member) => ) : null} {searchedMembers?.length === 0 && ( -

No matching member

+

No matching members

)}
); diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 3893d307d..839212d75 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -42,7 +42,7 @@ const profileLinks = (workspaceSlug: string, userId: string) => [ { name: "Settings", icon: Settings, - link: `/${workspaceSlug}/me/profile`, + link: "/me/profile", }, ]; @@ -99,7 +99,7 @@ export const WorkspaceSidebarDropdown = observer(() => { {({ open }) => ( <> - +
{ {!sidebarCollapsed && ( diff --git a/web/layouts/admin-layout/sidebar.tsx b/web/layouts/admin-layout/sidebar.tsx index d3a9ecfa1..ce6a302dd 100644 --- a/web/layouts/admin-layout/sidebar.tsx +++ b/web/layouts/admin-layout/sidebar.tsx @@ -13,7 +13,6 @@ export const InstanceAdminSidebar: FC = observer(() => { return (
= observer(() => { return (
= (props) => { + const { children, header } = props; + + return ( + <> + + +
+ +
+ {header} +
+
+ +
+ {children} +
+
+
+
+ + ); +}; diff --git a/web/layouts/settings-layout/profile/settings-sidebar.tsx b/web/layouts/settings-layout/profile/settings-sidebar.tsx new file mode 100644 index 000000000..1f87a0382 --- /dev/null +++ b/web/layouts/settings-layout/profile/settings-sidebar.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { useRouter } from "next/router"; +import Link from "next/link"; + +const PROFILE_LINKS: Array<{ + label: string; + href: string; +}> = [ + { + label: "Profile", + href: `/me/profile`, + }, + { + label: "Activity", + href: `/me/profile/activity`, + }, + { + label: "Preferences", + href: `/me/profile/preferences`, + }, +]; + +export const ProfileSettingsSidebar = () => { + const router = useRouter(); + + return ( +
+ My Account +
+ {PROFILE_LINKS.map((link) => ( + + +
+ {link.label} +
+
+ + ))} +
+
+ ); +}; diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx new file mode 100644 index 000000000..325fd703a --- /dev/null +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -0,0 +1,119 @@ +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +import { MoveLeft, Plus, UserPlus } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { Tooltip } from "@plane/ui"; + +const SIDEBAR_LINKS = [ + { + key: "create-workspace", + Icon: Plus, + name: "Create workspace", + href: "/create-workspace", + }, + { + key: "invitations", + Icon: UserPlus, + name: "Invitations", + href: "/invitations", + }, +]; + +export const ProfileLayoutSidebar = observer(() => { + const { + theme: { sidebarCollapsed, toggleSidebar }, + workspace: { workspaces }, + } = useMobxStore(); + + return ( +
+
+
+ {SIDEBAR_LINKS.map((link) => ( + + + +
+ {} + {!sidebarCollapsed && link.name} +
+
+
+ + ))} +
+ {workspaces && workspaces.length > 0 && ( + + )} +
+ + +
+
+
+ ); +}); diff --git a/web/layouts/settings-layout/project/index.tsx b/web/layouts/settings-layout/project/index.ts similarity index 100% rename from web/layouts/settings-layout/project/index.tsx rename to web/layouts/settings-layout/project/index.ts diff --git a/web/layouts/settings-layout/workspace/index.tsx b/web/layouts/settings-layout/workspace/index.ts similarity index 100% rename from web/layouts/settings-layout/workspace/index.tsx rename to web/layouts/settings-layout/workspace/index.ts diff --git a/web/layouts/settings-layout/workspace/sidebar.tsx b/web/layouts/settings-layout/workspace/sidebar.tsx index 831789bbc..19e9aabd8 100644 --- a/web/layouts/settings-layout/workspace/sidebar.tsx +++ b/web/layouts/settings-layout/workspace/sidebar.tsx @@ -64,31 +64,6 @@ export const WorkspaceSettingsSidebar = () => { }, ]; - const profileLinks: Array<{ - label: string; - href: string; - }> = [ - { - label: "Profile", - href: `/${workspaceSlug}/me/profile`, - }, - { - label: "Activity", - href: `/${workspaceSlug}/me/profile/activity`, - }, - { - label: "Preferences", - href: `/${workspaceSlug}/me/profile/preferences`, - }, - ]; - - function highlightSetting(label: string, link: string): boolean { - if (router.asPath.startsWith(link) && (label === "Imports" || label === "Api tokens")) { - return true; - } - return link === router.asPath; - } - return (
@@ -114,26 +89,6 @@ export const WorkspaceSettingsSidebar = () => { )}
-
- My Account -
- {profileLinks.map((link) => ( - - -
- {link.label} -
-
- - ))} -
-
); }; diff --git a/web/layouts/profile-layout/index.ts b/web/layouts/user-profile-layout/index.ts similarity index 100% rename from web/layouts/profile-layout/index.ts rename to web/layouts/user-profile-layout/index.ts diff --git a/web/layouts/profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx similarity index 100% rename from web/layouts/profile-layout/layout.tsx rename to web/layouts/user-profile-layout/layout.tsx diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index 6d0c6e0d6..2d1bb5534 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -4,7 +4,7 @@ import useSWR from "swr"; import { observer } from "mobx-react-lite"; // layouts import { AppLayout } from "layouts/app-layout"; -import { ProfileAuthWrapper } from "layouts/profile-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components import { UserProfileHeader } from "components/headers"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; @@ -65,7 +65,7 @@ const ProfileAssignedIssuesPage: NextPageWithLayout = observer(() => { ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( - }> + }> {page} ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx index 7bf21edd9..a6bbd8521 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { AppLayout } from "layouts/app-layout"; -import { ProfileAuthWrapper } from "layouts/profile-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components import { UserProfileHeader } from "components/headers"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; @@ -61,7 +61,7 @@ const ProfileCreatedIssuesPage: NextPageWithLayout = () => { ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( - }> + }> {page} ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index f345f8867..22fec985b 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -5,7 +5,7 @@ import useSWR from "swr"; import { UserService } from "services/user.service"; // layouts import { AppLayout } from "layouts/app-layout"; -import { ProfileAuthWrapper } from "layouts/profile-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components import { UserProfileHeader } from "components/headers"; import { @@ -56,7 +56,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => { ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) { return ( - }> + }> {page} ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx index 8900fb3fd..bcc9c66a5 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { AppLayout } from "layouts/app-layout"; -import { ProfileAuthWrapper } from "layouts/profile-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components import { UserProfileHeader } from "components/headers"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; @@ -61,7 +61,7 @@ const ProfileSubscribedIssuesPage: NextPageWithLayout = () => { ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( - }> + }> {page} ); diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index 93aba3db1..56a9baab3 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -14,10 +14,11 @@ import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/w import { Button } from "@plane/ui"; // icons import { Search } from "lucide-react"; +// helpers +import { trackEvent } from "helpers/event-tracker.helper"; // types import { NextPageWithLayout } from "types/app"; import { IWorkspaceBulkInviteFormData } from "types"; -import { trackEvent } from "helpers/event-tracker.helper"; const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const router = useRouter(); @@ -36,7 +37,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { if (!workspaceSlug) return; return inviteMembersToWorkspace(workspaceSlug.toString(), data) - .then(async (res) => { + .then(async () => { setInviteModal(false); trackEvent("WORKSPACE_USER_INVITE"); setToastAlert({ @@ -67,14 +68,14 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {

Members

-
- +
+ setSearchQuery(e.target.value)} + autoFocus />
+
+
+ + + + )} + +
); }; -ProfilePage.getLayout = function getLayout(page: ReactElement) { - return ( - }> - {page} - - ); +ProfileSettingsPage.getLayout = function getLayout(page: ReactElement) { + return }>{page}; }; -export default ProfilePage; +export default ProfileSettingsPage; diff --git a/web/pages/[workspaceSlug]/me/profile/preferences.tsx b/web/pages/me/profile/preferences.tsx similarity index 87% rename from web/pages/[workspaceSlug]/me/profile/preferences.tsx rename to web/pages/me/profile/preferences.tsx index d08b9da3c..57631488a 100644 --- a/web/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/web/pages/me/profile/preferences.tsx @@ -5,11 +5,10 @@ import { useTheme } from "next-themes"; import { useMobxStore } from "lib/mobx/store-provider"; import useToast from "hooks/use-toast"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { WorkspaceSettingLayout } from "layouts/settings-layout"; +import { ProfileSettingsLayout } from "layouts/settings-layout"; // components import { CustomThemeSelector, ThemeSwitch } from "components/core"; -import { WorkspaceSettingHeader } from "components/headers"; +import { ProfileSettingsHeader } from "components/headers"; // ui import { Spinner } from "@plane/ui"; // constants @@ -76,11 +75,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => { }); ProfilePreferencesPage.getLayout = function getLayout(page: ReactElement) { - return ( - }> - {page} - - ); + return }>{page}; }; export default ProfilePreferencesPage; diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index 3f204ebaf..517e151f6 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -22,8 +22,8 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import { IUser, TOnboardingSteps } from "types"; import { NextPageWithLayout } from "types/app"; import { ChevronDown } from "lucide-react"; -import { Menu, Popover, Transition } from "@headlessui/react"; -import DeleteAccountModal from "components/account/delete-account-modal"; +import { Menu, Transition } from "@headlessui/react"; +import { DeactivateAccountModal } from "components/account"; import { useRouter } from "next/router"; // services @@ -101,12 +101,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { return ( <> - { - setShowDeleteModal(false); - }} - /> + setShowDeleteModal(false)} /> {user && step !== null ? (
diff --git a/web/services/project/project-member.service.ts b/web/services/project/project-member.service.ts index 5f3e4710a..3612a185e 100644 --- a/web/services/project/project-member.service.ts +++ b/web/services/project/project-member.service.ts @@ -61,28 +61,4 @@ export class ProjectMemberService extends APIService { throw error?.response?.data; }); } - - async fetchProjectInvitations(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async updateProjectInvitation(workspaceSlug: string, projectId: string, invitationId: string): Promise { - return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/${invitationId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteProjectInvitation(workspaceSlug: string, projectId: string, invitationId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/${invitationId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } } diff --git a/web/services/project/project.service.ts b/web/services/project/project.service.ts index 5fb0e4a10..7e8821cf5 100644 --- a/web/services/project/project.service.ts +++ b/web/services/project/project.service.ts @@ -68,22 +68,6 @@ export class ProjectService extends APIService { }); } - async joinProject(workspaceSlug: string, project_ids: string[]): Promise { - return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/invitations/`, { project_ids }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async leaveProject(workspaceSlug: string, projectId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async setProjectView( workspaceSlug: string, projectId: string, diff --git a/web/services/user.service.ts b/web/services/user.service.ts index bfa97c1f1..4b9037287 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -96,8 +96,8 @@ export class UserService extends APIService { }); } - async getUserWorkspaceActivity(workspaceSlug: string): Promise { - return this.get(`/api/users/workspaces/${workspaceSlug}/activities/`) + async getUserActivity(): Promise { + return this.get(`/api/users/me/activities/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -185,12 +185,35 @@ export class UserService extends APIService { }); } - async deleteAccount(): Promise { - return this.delete("/api/users/me/") + async deactivateAccount() { + return this.delete(`/api/users/me/`) .then((response) => response?.data) .catch((error) => { - throw error?.response; + throw error?.response?.data; }); } + async leaveWorkspace(workspaceSlug: string) { + return this.post(`/api/workspaces/${workspaceSlug}/members/leave/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async joinProject(workspaceSlug: string, project_ids: string[]): Promise { + return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/invitations/`, { project_ids }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async leaveProject(workspaceSlug: string, projectId: string) { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } diff --git a/web/store/project/project-members.store.ts b/web/store/project/project-members.store.ts index d75bbeec6..cacc2c1ea 100644 --- a/web/store/project/project-members.store.ts +++ b/web/store/project/project-members.store.ts @@ -6,6 +6,9 @@ import { IProjectMember } from "types"; import { ProjectMemberService } from "services/project"; export interface IProjectMemberStore { + // states + error: any | null; + // observables members: { [projectId: string]: IProjectMember[] | null; // project_id: members @@ -28,6 +31,10 @@ export interface IProjectMemberStore { } export class ProjectMemberStore implements IProjectMemberStore { + // states + error: any | null = null; + + // observables members: { [projectId: string]: IProjectMember[]; // projectId: members } = {}; @@ -117,6 +124,7 @@ export class ProjectMemberStore implements IProjectMemberStore { */ removeMemberFromProject = async (workspaceSlug: string, projectId: string, memberId: string) => { const originalMembers = this.projectMembers || []; + try { runInAction(() => { this.members = { @@ -124,17 +132,20 @@ export class ProjectMemberStore implements IProjectMemberStore { [projectId]: this.projectMembers?.filter((member) => member.id !== memberId) || [], }; }); + await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberId); await this.fetchProjectMembers(workspaceSlug, projectId); } catch (error) { - console.log("Failed to delete project from project store"); // revert back to original members in case of error runInAction(() => { + this.error = error; this.members = { ...this.members, [projectId]: originalMembers, }; }); + + throw error; } }; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 1a4d0bcfe..bfe82e1ce 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -39,8 +39,6 @@ export interface IProjectStore { orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number; updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise; - joinProject: (workspaceSlug: string, projectIds: string[]) => Promise; - leaveProject: (workspaceSlug: string, projectId: string) => Promise; createProject: (workspaceSlug: string, data: any) => Promise; updateProject: (workspaceSlug: string, projectId: string, data: Partial) => Promise; deleteProject: (workspaceSlug: string, projectId: string) => Promise; @@ -100,7 +98,6 @@ export class ProjectStore implements IProjectStore { updateProjectView: action, createProject: action, updateProject: action, - leaveProject: action, }); this.rootStore = _rootStore; @@ -295,57 +292,6 @@ export class ProjectStore implements IProjectStore { } }; - joinProject = async (workspaceSlug: string, projectIds: string[]) => { - const newPermissions: { [projectId: string]: boolean } = {}; - projectIds.forEach((projectId) => { - newPermissions[projectId] = true; - }); - - try { - this.loader = true; - this.error = null; - - const response = await this.projectService.joinProject(workspaceSlug, projectIds); - await this.fetchProjects(workspaceSlug); - - runInAction(() => { - this.rootStore.user.hasPermissionToProject = { - ...this.rootStore.user.hasPermissionToProject, - ...newPermissions, - }; - this.loader = false; - this.error = null; - }); - - return response; - } catch (error) { - this.loader = false; - this.error = error; - return error; - } - }; - - leaveProject = async (workspaceSlug: string, projectId: string) => { - try { - this.loader = true; - this.error = null; - - const response = await this.projectService.leaveProject(workspaceSlug, projectId); - await this.fetchProjects(workspaceSlug); - - runInAction(() => { - this.loader = false; - this.error = null; - }); - - return response; - } catch (error) { - this.loader = false; - this.error = error; - return error; - } - }; - createProject = async (workspaceSlug: string, data: any) => { try { const response = await this.projectService.createProject(workspaceSlug, data); diff --git a/web/store/user.store.ts b/web/store/user.store.ts index 177694f53..155413a46 100644 --- a/web/store/user.store.ts +++ b/web/store/user.store.ts @@ -53,6 +53,13 @@ export interface IUserStore { updateTourCompleted: () => Promise; updateCurrentUser: (data: Partial) => Promise; updateCurrentUserTheme: (theme: string) => Promise; + + deactivateAccount: () => Promise; + + leaveWorkspace: (workspaceSlug: string) => Promise; + + joinProject: (workspaceSlug: string, projectIds: string[]) => Promise; + leaveProject: (workspaceSlug: string, projectId: string) => Promise; } class UserStore implements IUserStore { @@ -110,6 +117,10 @@ class UserStore implements IUserStore { updateTourCompleted: action, updateCurrentUser: action, updateCurrentUserTheme: action, + deactivateAccount: action, + leaveWorkspace: action, + joinProject: action, + leaveProject: action, // computed currentProjectMemberInfo: computed, currentWorkspaceMemberInfo: computed, @@ -179,7 +190,7 @@ class UserStore implements IUserStore { if (response) { runInAction(() => { this.isUserInstanceAdmin = response.is_instance_admin; - }) + }); } return response.is_instance_admin; } catch (error) { @@ -350,6 +361,67 @@ class UserStore implements IUserStore { throw error; } }; + + deactivateAccount = async () => { + try { + await this.userService.deactivateAccount(); + } catch (error) { + throw error; + } + }; + + leaveWorkspace = async (workspaceSlug: string) => { + try { + await this.userService.leaveWorkspace(workspaceSlug); + + runInAction(() => { + delete this.workspaceMemberInfo[workspaceSlug]; + delete this.hasPermissionToWorkspace[workspaceSlug]; + }); + } catch (error) { + throw error; + } + }; + + joinProject = async (workspaceSlug: string, projectIds: string[]) => { + const newPermissions: { [projectId: string]: boolean } = {}; + projectIds.forEach((projectId) => { + newPermissions[projectId] = true; + }); + + try { + const response = await this.userService.joinProject(workspaceSlug, projectIds); + + runInAction(() => { + this.hasPermissionToProject = { + ...this.hasPermissionToProject, + ...newPermissions, + }; + }); + + return response; + } catch (error) { + throw error; + } + }; + + leaveProject = async (workspaceSlug: string, projectId: string) => { + const newPermissions: { [projectId: string]: boolean } = {}; + newPermissions[projectId] = false; + + try { + await this.userService.leaveProject(workspaceSlug, projectId); + + runInAction(() => { + this.hasPermissionToProject = { + ...this.hasPermissionToProject, + ...newPermissions, + }; + }); + } catch (error) { + throw error; + } + }; } export default UserStore; diff --git a/web/store/workspace/workspace-member.store.ts b/web/store/workspace/workspace-member.store.ts index 2f36e6eea..f8885716f 100644 --- a/web/store/workspace/workspace-member.store.ts +++ b/web/store/workspace/workspace-member.store.ts @@ -259,18 +259,14 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { const index = members.findIndex((m) => m.id === memberId); members.splice(index, 1); - // optimistic update - runInAction(() => { - this.members = { - ...this.members, - [workspaceSlug]: members, - }; - }); - try { runInAction(() => { this.loader = true; this.error = null; + this.members = { + ...this.members, + [workspaceSlug]: members, + }; }); await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId); diff --git a/web/types/issues.d.ts b/web/types/issues.d.ts index b04a7e5ef..fdba7fd43 100644 --- a/web/types/issues.d.ts +++ b/web/types/issues.d.ts @@ -209,6 +209,7 @@ export interface IIssueActivity { updated_by: string; verb: string; workspace: string; + workspace_detail?: IWorkspaceLite; } export interface IIssueComment extends IIssueActivity { diff --git a/web/types/users.d.ts b/web/types/users.d.ts index b82daa75f..0ac13fd34 100644 --- a/web/types/users.d.ts +++ b/web/types/users.d.ts @@ -27,7 +27,7 @@ export interface IUser { user_timezone: string; username: string; theme: IUserTheme; - use_case? :string; + use_case?: string; } export interface IInstanceAdminStatus {