chore: deactivate user option added (#2841)

* dev: deactivate user option added

* chore: new layout for profile settings

* fix: build errors

* fix: user profile activity
This commit is contained in:
Aaryan Khandelwal 2023-11-23 14:44:06 +05:30 committed by sriram veeraghanta
parent 3c89ef8cc3
commit db75eced0a
53 changed files with 799 additions and 625 deletions

View file

@ -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> = (props) => {
export const DeactivateAccountModal: React.FC<Props> = (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> = (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> = (props) => {
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="">
<div className="flex items-center gap-x-4">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<Trash2 className="h-5 w-5 text-red-600" aria-hidden="true" />
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
Not the right workspace?
Deactivate account?
</Dialog.Title>
</div>
<div className="mt-6 px-4">
<ul className="text-onboarding-text-300 list-disc font-normal text-base">
<li>Delete this account if you have another and wont use this account.</li>
<li>Switch to another account if youd like to come back to this account another time.</li>
<li>Deactivate this account if you have another and won{"'"}t use this account.</li>
<li>Switch to another account if you{"'"}d like to come back to this account another time.</li>
</ul>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-4 p-4 mb-2 sm:px-6">
<span className="text-sm font-medium hover:cursor-pointer" onClick={handleSignOut}>
Switch account
</span>
<button
className="py-1.5 px-3 font-medium rounded-sm text-red-500 border border-red-500 text-sm "
onClick={handleDeleteAccount}
>
{isDeleteLoading ? "Deleting..." : "Delete account"}
</button>
<div className="flex items-center justify-end gap-2 p-4 mb-2 sm:px-6">
<Button variant="link-primary" onClick={handleSwitchAccount} loading={switchingAccount}>
{switchingAccount ? "Switching..." : "Switch account"}
</Button>
<Button variant="outline-danger" onClick={handleDeleteAccount}>
{isDeactivating ? "Deactivating..." : "Deactivate account"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
@ -140,5 +149,3 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
</Transition.Root>
);
};
export default DeleteAccountModal;

View file

@ -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";

View file

@ -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);
});
};

View file

@ -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}
/>
<CommandModal />
</>

View file

@ -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 }) => {
<a
aria-disabled={activity.issue === null}
href={`${
activity.issue_detail ? `/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}` : "#"
activity.issue_detail
? `/${workspaceSlug ?? activity.workspace_detail?.slug}/projects/${activity.project}/issues/${
activity.issue
}`
: "#"
}`}
target={activity.issue === null ? "_self" : "_blank"}
rel={activity.issue === null ? "" : "noopener noreferrer"}
@ -63,7 +61,9 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
return (
<a
href={`/${workspaceSlug}/profile/${activity.new_identifier ?? activity.old_identifier}`}
href={`/${workspaceSlug ?? activity.workspace_detail?.slug}/profile/${
activity.new_identifier ?? activity.old_identifier
}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center hover:underline"
@ -73,25 +73,27 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
);
};
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 (
<span
className="h-1.5 w-1.5 rounded-full flex-shrink-0"
style={{
backgroundColor: labels?.find((l) => 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: <CopyPlus size={12} color="#6b7280" />,
},
relates_to: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked that this issue relates to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
},
cycles: {
message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created")
@ -365,13 +349,13 @@ const activityDetails: {
icon: <LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
},
labels: {
message: (activity, showIssue) => {
message: (activity, showIssue, workspaceSlug) => {
if (activity.old_value === "")
return (
<>
added a new label{" "}
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<LabelPill labelId={activity.new_identifier ?? ""} />
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap">
<LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
</span>
{showIssue && (
@ -387,7 +371,7 @@ const activityDetails: {
<>
removed the label{" "}
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<LabelPill labelId={activity.old_identifier ?? ""} />
<LabelPill labelId={activity.old_identifier ?? ""} workspaceSlug={workspaceSlug} />
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
</span>
{showIssue && (
@ -586,6 +570,24 @@ const activityDetails: {
),
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
},
relates_to: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked that this issue relates to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
},
start_date: {
message: (activity, showIssue) => {
if (!activity.new_value)
@ -675,7 +677,12 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
);
export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIssueActivity; showIssue?: boolean }) => {
type ActivityMessageProps = {
activity: IIssueActivity;
showIssue?: boolean;
};
export const ActivityMessage = ({ activity, showIssue = false }: ActivityMessageProps) => {
const router = useRouter();
const { workspaceSlug } = router.query;
@ -684,7 +691,7 @@ export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIs
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
activity,
showIssue,
workspaceSlug?.toString() ?? ""
workspaceSlug ? workspaceSlug.toString() : activity.workspace_detail?.slug ?? ""
)}
</>
);

View file

@ -9,6 +9,7 @@ export * from "./workspace-analytics";
export * from "./workspace-dashboard";
export * from "./projects";
export * from "./profile-preferences";
export * from "./profile-settings";
export * from "./cycles";
export * from "./modules-list";
export * from "./project-settings";

View file

@ -0,0 +1,30 @@
import { FC } from "react";
// ui
import { Breadcrumbs } from "@plane/ui";
import { Settings } from "lucide-react";
interface IProfileSettingHeader {
title: string;
}
export const ProfileSettingsHeader: FC<IProfileSettingHeader> = (props) => {
const { title } = props;
return (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
label="My Profile"
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
link={`/me/profile`}
/>
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</Breadcrumbs>
</div>
</div>
</div>
);
};

View file

@ -1,36 +1,14 @@
import { FC } from "react";
import { useRouter } from "next/router";
// ui
import { Breadcrumbs } from "@plane/ui";
import { UserCircle2 } from "lucide-react";
// hooks
import { observer } from "mobx-react-lite";
export interface IUserProfileHeader {
title: string;
}
export const UserProfileHeader: FC<IUserProfileHeader> = observer((props) => {
const { title } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
label="Profile"
icon={<UserCircle2 className="h-4 w-4 text-custom-text-300" />}
link={`/${workspaceSlug}/me/profile`}
/>
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</Breadcrumbs>
</div>
export const UserProfileHeader = () => (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem type="text" label="Activity Overview" link="/me/profile" />
</Breadcrumbs>
</div>
</div>
);
});
</div>
);

View file

@ -25,7 +25,7 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
{
name: "Settings",
icon: Settings,
link: `/${workspaceSlug}/me/profile`,
link: `/me/profile`,
},
];

View file

@ -75,9 +75,9 @@ export const DeleteIssueModal: React.FC<Props> = observer((props) => {
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
</div>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
</span>

View file

@ -67,7 +67,7 @@ export const ProfileSidebar = () => {
<div className="relative h-32">
{user?.id === userId && (
<div className="absolute top-3.5 right-3.5 h-5 w-5 bg-white rounded grid place-items-center">
<Link href={`/${workspaceSlug}/me/profile`}>
<Link href="me/profile">
<a className="grid place-items-center text-black">
<Pencil className="h-3 w-3" />
</a>

View file

@ -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<void>;
isOpen: boolean;
onClose: () => void;
handleDelete: () => void;
data?: any;
};
export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
const { isOpen, onClose, data, handleDelete } = props;
export const ConfirmProjectMemberRemove: React.FC<Props> = 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> = (props) => {
const handleDeletion = async () => {
setIsDeleteLoading(true);
handleDelete();
await onSubmit();
handleClose();
};
const isCurrentUser = currentUser?.id === data?.id;
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -63,7 +76,7 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Remove {data?.display_name}?
{isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
@ -80,7 +93,13 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Removing..." : "Remove"}
{isCurrentUser
? isDeleteLoading
? "Leaving..."
: "Leave"
: isDeleteLoading
? "Removing..."
: "Remove"}
</Button>
</div>
</Dialog.Panel>
@ -90,4 +109,4 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
</Dialog>
</Transition.Root>
);
};
});

View file

@ -22,7 +22,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
// store
const {
project: { joinProject },
user: { joinProject },
} = useMobxStore();
// router
const router = useRouter();

View file

@ -35,7 +35,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = 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<ILeaveProjectModal> = 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`);

View file

@ -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<Props> = 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<any | null>(null);
const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState<any | null>(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 (
<>
<ConfirmProjectMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
onClose={() => {
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}
/>
<div className="group flex items-center justify-between px-3 py-4 hover:bg-custom-background-90">
<div className="flex items-center gap-x-4 gap-y-2">
{member.avatar && member.avatar !== "" ? (
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
{memberDetails.avatar && memberDetails.avatar !== "" ? (
<Link href={`/${workspaceSlug}/profile/${memberDetails.id}`}>
<a className="relative flex h-10 w-10 items-center justify-center rounded p-4 capitalize text-white">
<img
src={member.avatar}
alt={member.display_name || member.email}
src={memberDetails.avatar}
alt={memberDetails.display_name || memberDetails.email}
className="absolute top-0 left-0 h-full w-full object-cover rounded"
/>
</a>
</Link>
) : (
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<Link href={`/${workspaceSlug}/profile/${memberDetails.id}`}>
<a className="relative flex h-10 w-10 items-center justify-center rounded p-4 capitalize bg-gray-700 text-white">
{(member.display_name ?? member.email ?? "?")[0]}
{(memberDetails.display_name ?? memberDetails.email ?? "?")[0]}
</a>
</Link>
)}
<div>
{member.member ? (
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<a className="text-sm font-medium">
{member.first_name} {member.last_name}
</a>
</Link>
) : (
<h4 className="text-sm cursor-default">{member.display_name || member.email}</h4>
)}
<Link href={`/${workspaceSlug}/profile/${memberDetails.id}`}>
<a className="text-sm font-medium">
{memberDetails.first_name} {memberDetails.last_name}
</a>
</Link>
<div className="flex items-center">
<p className="text-xs text-custom-text-300">{member.display_name}</p>
<p className="text-xs text-custom-text-300">{memberDetails.display_name}</p>
{isAdmin && (
<>
<Dot height={16} width={16} className="text-custom-text-300" />
<p className="text-xs text-custom-text-300">{member.email}</p>
<p className="text-xs text-custom-text-300">{memberDetails.email}</p>
</>
)}
</div>
@ -129,23 +111,17 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
</div>
<div className="flex items-center gap-2 text-xs">
{!member?.status && (
<div className="flex items-center justify-center rounded bg-yellow-500/20 px-2.5 py-1 text-center text-xs text-yellow-500 font-medium">
<p>Pending</p>
</div>
)}
<CustomSelect
customButton={
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
<span
className={`flex items-center text-xs font-medium rounded ${
member.memberId !== currentProjectMemberInfo?.id ? "" : "text-custom-sidebar-text-400"
memberDetails.id !== currentProjectMemberInfo?.id ? "" : "text-custom-sidebar-text-400"
}`}
>
{ROLE[member.role as keyof typeof ROLE]}
</span>
{member.memberId !== currentProjectMemberInfo?.id && (
{memberDetails.id !== currentProjectMemberInfo?.id && (
<span className="grid place-items-center">
<ChevronDown className="h-3 w-3" />
</span>
@ -170,9 +146,9 @@ export const ProjectMemberListItem: React.FC<Props> = 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<Props> = observer((props) => {
</CustomSelect>
{isAdmin && (
<Tooltip
tooltipContent={member.memberId === currentProjectMemberInfo?.member ? "Leave project" : "Remove member"}
tooltipContent={
memberDetails.id === currentProjectMemberInfo?.member.id ? "Leave project" : "Remove member"
}
>
<button
type="button"
onClick={() => {
if (member.member) setSelectedRemoveMember(member);
else setSelectedInviteRemoveMember(member);
}}
onClick={() => setRemoveMemberModal(true)}
className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
>
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />

View file

@ -1,12 +1,9 @@
import { useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { mutate } from "swr";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { ProjectMemberService } from "services/project";
// hooks
import useUser from "hooks/use-user";
// components
import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project";
// ui
@ -14,9 +11,6 @@ import { Button, Loader } from "@plane/ui";
// icons
import { Search } from "lucide-react";
// services
const projectInvitationService = new ProjectMemberService();
export const ProjectMemberList: React.FC = observer(() => {
// router
const router = useRouter();
@ -29,49 +23,12 @@ export const ProjectMemberList: React.FC = observer(() => {
// states
const [inviteModal, setInviteModal] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>("");
const [searchQuery, setSearchQuery] = useState("");
const { user } = useUser();
const searchedMembers = (projectMembers ?? []).filter((member) => {
const fullName = `${member.member.first_name} ${member.member.last_name}`.toLowerCase();
const displayName = member.member.display_name.toLowerCase();
const { data: projectInvitations } = useSWR(
workspaceSlug && projectId ? `PROJECT_INVITATIONS_${projectId.toString()}` : null,
workspaceSlug && projectId
? () => projectInvitationService.fetchProjectInvitations(workspaceSlug.toString(), projectId.toString())
: null
);
// derived values
const members = [
...(projectMembers?.map((item) => ({
id: item.id,
memberId: item.member?.id,
avatar: item.member?.avatar,
first_name: item.member?.first_name,
last_name: item.member?.last_name,
email: item.member?.email,
display_name: item.member?.display_name,
role: item.role,
status: true,
member: true,
})) || []),
...(projectInvitations?.map((item: any) => ({
id: item.id,
memberId: item.id,
avatar: item.avatar ?? "",
first_name: item.first_name ?? item.email,
last_name: item.last_name ?? "",
email: item.email,
display_name: item.email,
role: item.role,
status: item.accepted,
member: false,
})) || []),
];
const searchedMembers = members?.filter((member) => {
const fullName = `${member.first_name} ${member.last_name}`.toLowerCase();
const displayName = member.display_name.toLowerCase();
return displayName.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
});
@ -79,9 +36,8 @@ export const ProjectMemberList: React.FC = observer(() => {
<>
<SendProjectInvitationModal
isOpen={inviteModal}
setIsOpen={setInviteModal}
members={members}
user={user}
members={projectMembers ?? []}
onClose={() => setInviteModal(false)}
onSuccess={() => {
mutate(`PROJECT_INVITATIONS_${projectId?.toString()}`);
fetchProjectMembers(workspaceSlug?.toString()!, projectId?.toString()!);
@ -104,7 +60,7 @@ export const ProjectMemberList: React.FC = observer(() => {
Add Member
</Button>
</div>
{!projectMembers || !projectInvitations ? (
{!projectMembers ? (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
@ -113,7 +69,7 @@ export const ProjectMemberList: React.FC = observer(() => {
</Loader>
) : (
<div className="divide-y divide-custom-border-100">
{members.length > 0
{projectMembers.length > 0
? searchedMembers.map((member) => <ProjectMemberListItem key={member.id} member={member} />)
: null}
{searchedMembers.length === 0 && (

View file

@ -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<React.SetStateAction<boolean>>;
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<Props> = 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<Props> = 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<Props> = 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<Props> = observer((props) => {
};
const handleClose = () => {
setIsOpen(false);
onClose();
const timeout = setTimeout(() => {
reset(defaultValues);
clearTimeout(timeout);
@ -195,7 +186,7 @@ export const SendProjectInvitationModal: React.FC<Props> = 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 (
<CustomSearchSelect
@ -250,7 +241,7 @@ export const SendProjectInvitationModal: React.FC<Props> = 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 (
<CustomSelect.Option key={key} value={key}>

View file

@ -26,7 +26,11 @@ export const ProjectSidebarList: FC = observer(() => {
// refs
const containerRef = useRef<HTMLDivElement | null>(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(() => {
<button
type="button"
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"
onClick={() => commandPaletteStore.toggleCreateProjectModal(true)}
onClick={() => toggleCreateProjectModal(true)}
>
<Plus className="h-5 w-5" />
{!isCollapsed && "Add Project"}

View file

@ -8,14 +8,14 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { Button } from "@plane/ui";
type Props = {
data?: any;
isOpen: boolean;
onClose: () => void;
onSubmit: () => Promise<void>;
data?: any;
};
export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((props) => {
const { isOpen, onClose, data, onSubmit } = props;
const { data, isOpen, onClose, onSubmit } = props;
const [isRemoving, setIsRemoving] = useState(false);
@ -62,8 +62,8 @@ export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((props) =>
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-custom-background-100 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] bg-custom-background-100">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
@ -89,12 +89,18 @@ export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((props) =>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6 bg-custom-background-100">
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
{currentUser?.id === data?.memberId
? isRemoving
? "Leaving..."
: "Leave"
: isRemoving
? "Removing..."
: "Remove"}
</Button>
</div>
</Dialog.Panel>

View file

@ -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<WorkspaceHelpSectionProps> = 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<WorkspaceHelpSectionProps> = observe
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
const isCollapsed = themeStore.sidebarCollapsed || false;
const isCollapsed = sidebarCollapsed || false;
return (
<>
@ -71,7 +73,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = 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)}
>
<Zap className="h-3.5 w-3.5" />
</button>
@ -87,7 +89,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
<button
type="button"
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 md:hidden"
onClick={() => themeStore.toggleSidebar()}
onClick={() => toggleSidebar()}
>
<MoveLeft className="h-3.5 w-3.5" />
</button>
@ -96,7 +98,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = 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()}
>
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
</button>

View file

@ -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> = (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> = (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> = (props) => {
isOpen={removeMemberModal}
onClose={() => setRemoveMemberModal(false)}
data={member}
onSubmit={handleRemoveMember}
onSubmit={handleRemove}
/>
<div className="group flex items-center justify-between px-3 py-4 hover:bg-custom-background-90">
<div className="flex items-center gap-x-4 gap-y-2">

View file

@ -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) => <WorkspaceMembersListItem key={member.id} member={member} />)
: null}
{searchedMembers?.length === 0 && (
<h4 className="text-md text-custom-text-400 text-center mt-20">No matching member</h4>
<h4 className="text-sm text-custom-text-400 text-center mt-16">No matching members</h4>
)}
</div>
);

View file

@ -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(() => {
<Menu as="div" className="relative col-span-4 text-left flex-grow h-full truncate">
{({ open }) => (
<>
<Menu.Button className="text-custom-sidebar-text-200 rounded-md hover:bg-custom-sidebar-background-80 text-sm font-medium focus:outline-none w-full h-full truncate">
<Menu.Button className="group/menu-button text-custom-sidebar-text-200 rounded-md hover:bg-custom-sidebar-background-80 text-sm font-medium focus:outline-none w-full h-full truncate">
<div
className={`flex items-center justify-between gap-x-2 rounded p-1 truncate ${
sidebarCollapsed ? "justify-center" : ""
@ -131,7 +131,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
{!sidebarCollapsed && (
<ChevronDown
className={`h-4 w-4 mx-1 flex-shrink-0 ${
className={`hidden group-hover/menu-button:block h-4 w-4 mx-1 flex-shrink-0 ${
open ? "rotate-180" : ""
} text-custom-sidebar-text-400 duration-300`}
/>