forked from github/plane
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:
parent
9ba724b78d
commit
a7d6b528bd
@ -1,18 +1,17 @@
|
|||||||
// react
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// next
|
|
||||||
import { useRouter } from "next/router";
|
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
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "services/auth.service";
|
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 = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -20,23 +19,40 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const authService = new AuthService();
|
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 { 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 router = useRouter();
|
||||||
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleClose = () => {
|
||||||
|
setSwitchingAccount(false);
|
||||||
|
setIsDeactivating(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchAccount = async () => {
|
||||||
|
setSwitchingAccount(true);
|
||||||
|
|
||||||
await authService
|
await authService
|
||||||
.signOut()
|
.signOut()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate("CURRENT_USER_DETAILS", null);
|
mutate("CURRENT_USER_DETAILS", null);
|
||||||
setTheme("system");
|
setTheme("system");
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
handleClose();
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -44,35 +60,31 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Failed to sign out. Please try again.",
|
message: "Failed to sign out. Please try again.",
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
.finally(() => setSwitchingAccount(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteAccount = async () => {
|
const handleDeleteAccount = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeactivating(true);
|
||||||
await userService
|
|
||||||
.deleteAccount()
|
await deactivateAccount()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Account deleted successfully.",
|
message: "Account deleted successfully.",
|
||||||
});
|
});
|
||||||
mutate("CURRENT_USER_DETAILS", null);
|
handleClose();
|
||||||
setTheme("system");
|
|
||||||
router.push("/");
|
router.push("/");
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.data?.error,
|
message: err?.error,
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
setIsDeleteLoading(false);
|
.finally(() => setIsDeactivating(false));
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex items-center gap-x-4">
|
<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">
|
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
|
||||||
<Trash2 className="h-5 w-5 text-red-600" aria-hidden="true" />
|
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
|
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
|
||||||
Not the right workspace?
|
Deactivate account?
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 px-4">
|
<div className="mt-6 px-4">
|
||||||
<ul className="text-onboarding-text-300 list-disc font-normal text-base">
|
<ul className="text-onboarding-text-300 list-disc font-normal text-base">
|
||||||
<li>Delete this account if you have another and won’t use this account.</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>
|
<li>Switch to another account if you{"'"}d like to come back to this account another time.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-4 p-4 mb-2 sm:px-6">
|
<div className="flex items-center justify-end gap-2 p-4 mb-2 sm:px-6">
|
||||||
<span className="text-sm font-medium hover:cursor-pointer" onClick={handleSignOut}>
|
<Button variant="link-primary" onClick={handleSwitchAccount} loading={switchingAccount}>
|
||||||
Switch account
|
{switchingAccount ? "Switching..." : "Switch account"}
|
||||||
</span>
|
</Button>
|
||||||
<button
|
<Button variant="outline-danger" onClick={handleDeleteAccount}>
|
||||||
className="py-1.5 px-3 font-medium rounded-sm text-red-500 border border-red-500 text-sm "
|
{isDeactivating ? "Deactivating..." : "Deactivate account"}
|
||||||
onClick={handleDeleteAccount}
|
</Button>
|
||||||
>
|
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete account"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
@ -140,5 +149,3 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
|
|||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteAccountModal;
|
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./deactivate-account-modal";
|
||||||
export * from "./email-code-form";
|
export * from "./email-code-form";
|
||||||
export * from "./email-password-form";
|
export * from "./email-password-form";
|
||||||
export * from "./email-forgot-password-form";
|
export * from "./email-forgot-password-form";
|
||||||
|
@ -13,7 +13,9 @@ import JoinProjectImg from "public/auth/project-not-authorized.svg";
|
|||||||
export const JoinProject: React.FC = () => {
|
export const JoinProject: React.FC = () => {
|
||||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||||
|
|
||||||
const { project: projectStore } = useMobxStore();
|
const {
|
||||||
|
user: { joinProject },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -23,7 +25,7 @@ export const JoinProject: React.FC = () => {
|
|||||||
|
|
||||||
setIsJoiningProject(true);
|
setIsJoiningProject(true);
|
||||||
|
|
||||||
projectStore.joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => {
|
joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => {
|
||||||
setIsJoiningProject(false);
|
setIsJoiningProject(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,6 @@ import useSWR from "swr";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
// components
|
// components
|
||||||
import { CommandModal, ShortcutsModal } from "components/command-palette";
|
import { CommandModal, ShortcutsModal } from "components/command-palette";
|
||||||
import { BulkDeleteIssuesModal } from "components/core";
|
import { BulkDeleteIssuesModal } from "components/core";
|
||||||
@ -30,7 +29,11 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
|
||||||
// store
|
// store
|
||||||
const { commandPalette, theme: themeStore } = useMobxStore();
|
const {
|
||||||
|
commandPalette,
|
||||||
|
theme: { toggleSidebar },
|
||||||
|
user: { currentUser },
|
||||||
|
} = useMobxStore();
|
||||||
const {
|
const {
|
||||||
toggleCommandPaletteModal,
|
toggleCommandPaletteModal,
|
||||||
isCreateIssueModalOpen,
|
isCreateIssueModalOpen,
|
||||||
@ -52,9 +55,6 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
isDeleteIssueModalOpen,
|
isDeleteIssueModalOpen,
|
||||||
toggleDeleteIssueModal,
|
toggleDeleteIssueModal,
|
||||||
} = commandPalette;
|
} = commandPalette;
|
||||||
const { toggleSidebar } = themeStore;
|
|
||||||
|
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [handleKeyDown]);
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
if (!user) return null;
|
if (!currentUser) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -223,7 +223,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
toggleBulkDeleteIssueModal(false);
|
toggleBulkDeleteIssueModal(false);
|
||||||
}}
|
}}
|
||||||
user={user}
|
user={currentUser}
|
||||||
/>
|
/>
|
||||||
<CommandModal />
|
<CommandModal />
|
||||||
</>
|
</>
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hook
|
// hook
|
||||||
import useEstimateOption from "hooks/use-estimate-option";
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
// services
|
|
||||||
import { IssueLabelService } from "services/issue";
|
|
||||||
// icons
|
// icons
|
||||||
import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui";
|
import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui";
|
||||||
import {
|
import {
|
||||||
@ -29,11 +27,7 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
|||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueActivity } from "types";
|
import { IIssueActivity } from "types";
|
||||||
// fetch-keys
|
import { useEffect } from "react";
|
||||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
// services
|
|
||||||
const issueLabelService = new IssueLabelService();
|
|
||||||
|
|
||||||
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -44,7 +38,11 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||||||
<a
|
<a
|
||||||
aria-disabled={activity.issue === null}
|
aria-disabled={activity.issue === null}
|
||||||
href={`${
|
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"}
|
target={activity.issue === null ? "_self" : "_blank"}
|
||||||
rel={activity.issue === null ? "" : "noopener noreferrer"}
|
rel={activity.issue === null ? "" : "noopener noreferrer"}
|
||||||
@ -63,7 +61,9 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="font-medium text-custom-text-100 inline-flex items-center hover:underline"
|
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 LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => {
|
||||||
const router = useRouter();
|
const {
|
||||||
const { workspaceSlug } = router.query;
|
workspace: { labels, fetchWorkspaceLabels },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const { data: labels } = useSWR(
|
const workspaceLabels = labels[workspaceSlug];
|
||||||
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
|
||||||
workspaceSlug ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) : null
|
useEffect(() => {
|
||||||
);
|
if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug);
|
||||||
|
}, [fetchWorkspaceLabels, workspaceLabels, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="h-1.5 w-1.5 rounded-full flex-shrink-0"
|
className="h-1.5 w-1.5 rounded-full flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: labels?.find((l) => l.id === labelId)?.color ?? "#000000",
|
backgroundColor: workspaceLabels?.find((l) => l.id === labelId)?.color ?? "#000000",
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
const EstimatePoint = ({ point }: { point: string }) => {
|
const EstimatePoint = ({ point }: { point: string }) => {
|
||||||
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
|
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
|
||||||
@ -243,24 +245,6 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <CopyPlus size={12} color="#6b7280" />,
|
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: {
|
cycles: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.verb === "created")
|
if (activity.verb === "created")
|
||||||
@ -365,13 +349,13 @@ const activityDetails: {
|
|||||||
icon: <LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
icon: <LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.old_value === "")
|
if (activity.old_value === "")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
added a new label{" "}
|
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">
|
<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 ?? ""} />
|
<LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
|
||||||
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
|
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
|
||||||
</span>
|
</span>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
@ -387,7 +371,7 @@ const activityDetails: {
|
|||||||
<>
|
<>
|
||||||
removed the label{" "}
|
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">
|
<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 className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
|
||||||
</span>
|
</span>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
@ -586,6 +570,24 @@ const activityDetails: {
|
|||||||
),
|
),
|
||||||
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
|
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: {
|
start_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
if (!activity.new_value)
|
if (!activity.new_value)
|
||||||
@ -675,7 +677,12 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
|
|||||||
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
|
<>{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 router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
@ -684,7 +691,7 @@ export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIs
|
|||||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
|
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
|
||||||
activity,
|
activity,
|
||||||
showIssue,
|
showIssue,
|
||||||
workspaceSlug?.toString() ?? ""
|
workspaceSlug ? workspaceSlug.toString() : activity.workspace_detail?.slug ?? ""
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,7 @@ export * from "./workspace-analytics";
|
|||||||
export * from "./workspace-dashboard";
|
export * from "./workspace-dashboard";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./profile-preferences";
|
export * from "./profile-preferences";
|
||||||
|
export * from "./profile-settings";
|
||||||
export * from "./cycles";
|
export * from "./cycles";
|
||||||
export * from "./modules-list";
|
export * from "./modules-list";
|
||||||
export * from "./project-settings";
|
export * from "./project-settings";
|
||||||
|
30
web/components/headers/profile-settings.tsx
Normal file
30
web/components/headers/profile-settings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,36 +1,14 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
import { UserCircle2 } from "lucide-react";
|
|
||||||
// hooks
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
export interface IUserProfileHeader {
|
export const UserProfileHeader = () => (
|
||||||
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="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 className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem type="text" label="Activity Overview" link="/me/profile" />
|
||||||
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>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
@ -25,7 +25,7 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
|
|||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
link: `/${workspaceSlug}/me/profile`,
|
link: `/me/profile`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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">
|
<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 flex-col gap-6 p-6">
|
||||||
<div className="flex w-full items-center justify-start gap-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" />
|
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||||
</span>
|
</div>
|
||||||
<span className="flex items-center justify-start">
|
<span className="flex items-center justify-start">
|
||||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
|
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
|
||||||
</span>
|
</span>
|
||||||
|
@ -67,7 +67,7 @@ export const ProfileSidebar = () => {
|
|||||||
<div className="relative h-32">
|
<div className="relative h-32">
|
||||||
{user?.id === userId && (
|
{user?.id === userId && (
|
||||||
<div className="absolute top-3.5 right-3.5 h-5 w-5 bg-white rounded grid place-items-center">
|
<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">
|
<a className="grid place-items-center text-black">
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// icons
|
// icons
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IUserLite } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
data: IUserLite;
|
||||||
|
onSubmit: () => Promise<void>;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleDelete: () => void;
|
|
||||||
data?: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
|
export const ConfirmProjectMemberRemove: React.FC<Props> = observer((props) => {
|
||||||
const { isOpen, onClose, data, handleDelete } = props;
|
const { data, onSubmit, isOpen, onClose } = props;
|
||||||
|
|
||||||
|
// states
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
user: { currentUser },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
@ -25,10 +34,14 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
handleDelete();
|
|
||||||
|
await onSubmit();
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCurrentUser = currentUser?.id === data?.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
@ -63,7 +76,7 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<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">
|
<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>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200">
|
||||||
@ -80,7 +93,13 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
|
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
|
||||||
{isDeleteLoading ? "Removing..." : "Remove"}
|
{isCurrentUser
|
||||||
|
? isDeleteLoading
|
||||||
|
? "Leaving..."
|
||||||
|
: "Leave"
|
||||||
|
: isDeleteLoading
|
||||||
|
? "Removing..."
|
||||||
|
: "Remove"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
@ -90,4 +109,4 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = (props) => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -22,7 +22,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
|||||||
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
|
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: { joinProject },
|
user: { joinProject },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -35,7 +35,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store
|
// store
|
||||||
const { project: projectStore } = useMobxStore();
|
const {
|
||||||
|
user: { leaveProject },
|
||||||
|
} = useMobxStore();
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -57,8 +59,7 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
if (data.projectName === project?.name) {
|
if (data.projectName === project?.name) {
|
||||||
if (data.confirmLeave === "Leave Project") {
|
if (data.confirmLeave === "Leave Project") {
|
||||||
return projectStore
|
return leaveProject(workspaceSlug.toString(), project.id)
|
||||||
.leaveProject(workspaceSlug.toString(), project.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
router.push(`/${workspaceSlug}/projects`);
|
router.push(`/${workspaceSlug}/projects`);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -15,113 +15,95 @@ import { ChevronDown, Dot, XCircle } from "lucide-react";
|
|||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
// types
|
// types
|
||||||
import { TUserProjectRole } from "types";
|
import { IProjectMember, TUserProjectRole } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
member: any;
|
member: IProjectMember;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
||||||
const { member } = props;
|
const { member } = props;
|
||||||
|
// states
|
||||||
|
const [removeMemberModal, setRemoveMemberModal] = useState(false);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// states
|
|
||||||
const [selectedRemoveMember, setSelectedRemoveMember] = useState<any | null>(null);
|
|
||||||
const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState<any | null>(null);
|
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
user: userStore,
|
user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject },
|
||||||
projectMember: {
|
projectMember: { removeMemberFromProject, updateMember },
|
||||||
projectMembers,
|
|
||||||
fetchProjectMembers,
|
|
||||||
removeMemberFromProject,
|
|
||||||
updateMember,
|
|
||||||
deleteProjectInvitation,
|
|
||||||
},
|
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
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
|
// derived values
|
||||||
const user = userStore.currentUser;
|
|
||||||
const { currentProjectMemberInfo, currentProjectRole } = userStore;
|
|
||||||
const isAdmin = currentProjectRole === 20;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfirmProjectMemberRemove
|
<ConfirmProjectMemberRemove
|
||||||
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
isOpen={removeMemberModal}
|
||||||
onClose={() => {
|
onClose={() => setRemoveMemberModal(false)}
|
||||||
setSelectedRemoveMember(null);
|
data={member.member}
|
||||||
setSelectedInviteRemoveMember(null);
|
onSubmit={handleRemove}
|
||||||
}}
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="group flex items-center justify-between px-3 py-4 hover:bg-custom-background-90">
|
<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">
|
<div className="flex items-center gap-x-4 gap-y-2">
|
||||||
{member.avatar && member.avatar !== "" ? (
|
{memberDetails.avatar && memberDetails.avatar !== "" ? (
|
||||||
<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 text-white">
|
<a className="relative flex h-10 w-10 items-center justify-center rounded p-4 capitalize text-white">
|
||||||
<img
|
<img
|
||||||
src={member.avatar}
|
src={memberDetails.avatar}
|
||||||
alt={member.display_name || member.email}
|
alt={memberDetails.display_name || memberDetails.email}
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</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">
|
<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>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{member.member ? (
|
<Link href={`/${workspaceSlug}/profile/${memberDetails.id}`}>
|
||||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
|
||||||
<a className="text-sm font-medium">
|
<a className="text-sm font-medium">
|
||||||
{member.first_name} {member.last_name}
|
{memberDetails.first_name} {memberDetails.last_name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
|
||||||
<h4 className="text-sm cursor-default">{member.display_name || member.email}</h4>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center">
|
<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 && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<Dot height={16} width={16} className="text-custom-text-300" />
|
<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>
|
</div>
|
||||||
@ -129,23 +111,17 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<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
|
<CustomSelect
|
||||||
customButton={
|
customButton={
|
||||||
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
|
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
|
||||||
<span
|
<span
|
||||||
className={`flex items-center text-xs font-medium rounded ${
|
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]}
|
{ROLE[member.role as keyof typeof ROLE]}
|
||||||
</span>
|
</span>
|
||||||
{member.memberId !== currentProjectMemberInfo?.id && (
|
{memberDetails.id !== currentProjectMemberInfo?.id && (
|
||||||
<span className="grid place-items-center">
|
<span className="grid place-items-center">
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
</span>
|
</span>
|
||||||
@ -170,9 +146,9 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
member.memberId === user?.id ||
|
memberDetails.id === currentUser?.id ||
|
||||||
!member.member ||
|
!member.member ||
|
||||||
(currentUser && currentUser.role !== 20 && currentUser.role < member.role)
|
(currentProjectRole && currentProjectRole !== 20 && currentProjectRole < member.role)
|
||||||
}
|
}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
>
|
>
|
||||||
@ -188,14 +164,13 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={member.memberId === currentProjectMemberInfo?.member ? "Leave project" : "Remove member"}
|
tooltipContent={
|
||||||
|
memberDetails.id === currentProjectMemberInfo?.member.id ? "Leave project" : "Remove member"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => setRemoveMemberModal(true)}
|
||||||
if (member.member) setSelectedRemoveMember(member);
|
|
||||||
else setSelectedInviteRemoveMember(member);
|
|
||||||
}}
|
|
||||||
className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
|
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} />
|
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
|
||||||
import { ProjectMemberService } from "services/project";
|
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
// components
|
// components
|
||||||
import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project";
|
import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
@ -14,9 +11,6 @@ import { Button, Loader } from "@plane/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
|
|
||||||
// services
|
|
||||||
const projectInvitationService = new ProjectMemberService();
|
|
||||||
|
|
||||||
export const ProjectMemberList: React.FC = observer(() => {
|
export const ProjectMemberList: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -29,49 +23,12 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
|
|
||||||
// states
|
// states
|
||||||
const [inviteModal, setInviteModal] = useState(false);
|
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());
|
return displayName.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,9 +36,8 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<SendProjectInvitationModal
|
<SendProjectInvitationModal
|
||||||
isOpen={inviteModal}
|
isOpen={inviteModal}
|
||||||
setIsOpen={setInviteModal}
|
members={projectMembers ?? []}
|
||||||
members={members}
|
onClose={() => setInviteModal(false)}
|
||||||
user={user}
|
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
mutate(`PROJECT_INVITATIONS_${projectId?.toString()}`);
|
mutate(`PROJECT_INVITATIONS_${projectId?.toString()}`);
|
||||||
fetchProjectMembers(workspaceSlug?.toString()!, projectId?.toString()!);
|
fetchProjectMembers(workspaceSlug?.toString()!, projectId?.toString()!);
|
||||||
@ -104,7 +60,7 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
Add Member
|
Add Member
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!projectMembers || !projectInvitations ? (
|
{!projectMembers ? (
|
||||||
<Loader className="space-y-5">
|
<Loader className="space-y-5">
|
||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
@ -113,7 +69,7 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-custom-border-100">
|
<div className="divide-y divide-custom-border-100">
|
||||||
{members.length > 0
|
{projectMembers.length > 0
|
||||||
? searchedMembers.map((member) => <ProjectMemberListItem key={member.id} member={member} />)
|
? searchedMembers.map((member) => <ProjectMemberListItem key={member.id} member={member} />)
|
||||||
: null}
|
: null}
|
||||||
{searchedMembers.length === 0 && (
|
{searchedMembers.length === 0 && (
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
|
||||||
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { ChevronDown, Plus, X } from "lucide-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";
|
import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui";
|
||||||
// services
|
// services
|
||||||
import { ProjectMemberService } from "services/project";
|
import { ProjectMemberService } from "services/project";
|
||||||
import { WorkspaceService } from "services/workspace.service";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
// helpers
|
||||||
|
import { trackEvent } from "helpers/event-tracker.helper";
|
||||||
// types
|
// types
|
||||||
import { IUser, TUserProjectRole } from "types";
|
import { IProjectMember, TUserProjectRole } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
import { trackEvent } from "helpers/event-tracker.helper";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
members: IProjectMember[];
|
||||||
members: any[];
|
onClose: () => void;
|
||||||
user: IUser | undefined;
|
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,23 +46,19 @@ const defaultValues: FormValues = {
|
|||||||
|
|
||||||
// services
|
// services
|
||||||
const projectMemberService = new ProjectMemberService();
|
const projectMemberService = new ProjectMemberService();
|
||||||
const workspaceService = new WorkspaceService();
|
|
||||||
|
|
||||||
export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
||||||
const { isOpen, setIsOpen, members, onSuccess } = props;
|
const { isOpen, members, onClose, onSuccess } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { user: userStore } = useMobxStore();
|
const {
|
||||||
const userRole = userStore.currentProjectRole;
|
user: { currentProjectRole },
|
||||||
|
workspaceMember: { workspaceMembers },
|
||||||
const { data: people } = useSWR(
|
} = useMobxStore();
|
||||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
|
|
||||||
workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug as string) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
@ -80,8 +72,8 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
name: "members",
|
name: "members",
|
||||||
});
|
});
|
||||||
|
|
||||||
const uninvitedPeople = people?.filter((person) => {
|
const uninvitedPeople = workspaceMembers?.filter((person) => {
|
||||||
const isInvited = members?.find((member) => member.memberId === person.member.id);
|
const isInvited = members?.find((member) => member.member.id === person.member.id);
|
||||||
|
|
||||||
return !isInvited;
|
return !isInvited;
|
||||||
});
|
});
|
||||||
@ -93,17 +85,15 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
await projectMemberService
|
await projectMemberService
|
||||||
.bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
|
.bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
setIsOpen(false);
|
onSuccess();
|
||||||
trackEvent(
|
onClose();
|
||||||
'PROJECT_MEMBER_INVITE',
|
trackEvent("PROJECT_MEMBER_INVITE");
|
||||||
)
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Member added successfully",
|
message: "Member added successfully",
|
||||||
});
|
});
|
||||||
onSuccess();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -114,7 +104,8 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
onClose();
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@ -195,7 +186,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
name={`members.${index}.member_id`}
|
name={`members.${index}.member_id`}
|
||||||
rules={{ required: "Please select a member" }}
|
rules={{ required: "Please select a member" }}
|
||||||
render={({ field: { value, onChange } }) => {
|
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 (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
@ -250,7 +241,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
width="w-full"
|
width="w-full"
|
||||||
>
|
>
|
||||||
{Object.entries(ROLE).map(([key, label]) => {
|
{Object.entries(ROLE).map(([key, label]) => {
|
||||||
if (parseInt(key) > (userRole ?? 5)) return null;
|
if (parseInt(key) > (currentProjectRole ?? 5)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSelect.Option key={key} value={key}>
|
<CustomSelect.Option key={key} value={key}>
|
||||||
|
@ -26,7 +26,11 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
// refs
|
// refs
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
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
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -34,9 +38,6 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const joinedProjects = workspaceSlug && projectStore.joinedProjects;
|
|
||||||
const favoriteProjects = workspaceSlug && projectStore.favoriteProjects;
|
|
||||||
|
|
||||||
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
|
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
|
||||||
? orderArrayBy(joinedProjects, "sort_order", "ascending")
|
? orderArrayBy(joinedProjects, "sort_order", "ascending")
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -62,11 +63,9 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
|
|
||||||
if (source.index === destination.index) return;
|
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(() => {
|
||||||
.updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder })
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -75,7 +74,7 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCollapsed = themeStore.sidebarCollapsed || false;
|
const isCollapsed = sidebarCollapsed || false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementing scroll animation styles based on the scroll length of the container
|
* Implementing scroll animation styles based on the scroll length of the container
|
||||||
@ -263,7 +262,7 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"
|
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" />
|
<Plus className="h-5 w-5" />
|
||||||
{!isCollapsed && "Add Project"}
|
{!isCollapsed && "Add Project"}
|
||||||
|
@ -8,14 +8,14 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
data?: any;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: () => Promise<void>;
|
onSubmit: () => Promise<void>;
|
||||||
data?: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((props) => {
|
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);
|
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"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
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]">
|
<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="bg-custom-background-100 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<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">
|
<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" />
|
<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>
|
||||||
</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}>
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -2,7 +2,6 @@ import React, { useRef, useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
@ -43,7 +42,10 @@ export interface WorkspaceHelpSectionProps {
|
|||||||
|
|
||||||
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
||||||
// store
|
// store
|
||||||
const { theme: themeStore, commandPalette: commandPaletteStore } = useMobxStore();
|
const {
|
||||||
|
theme: { sidebarCollapsed, toggleSidebar },
|
||||||
|
commandPalette: { toggleShortcutModal },
|
||||||
|
} = useMobxStore();
|
||||||
// states
|
// states
|
||||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
@ -51,7 +53,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
|||||||
|
|
||||||
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
|
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
|
||||||
|
|
||||||
const isCollapsed = themeStore.sidebarCollapsed || false;
|
const isCollapsed = sidebarCollapsed || false;
|
||||||
|
|
||||||
return (
|
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 ${
|
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" : ""
|
isCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => commandPaletteStore.toggleShortcutModal(true)}
|
onClick={() => toggleShortcutModal(true)}
|
||||||
>
|
>
|
||||||
<Zap className="h-3.5 w-3.5" />
|
<Zap className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@ -87,7 +89,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
|||||||
<button
|
<button
|
||||||
type="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"
|
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" />
|
<MoveLeft className="h-3.5 w-3.5" />
|
||||||
</button>
|
</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 ${
|
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" : ""
|
isCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => themeStore.toggleSidebar()}
|
onClick={() => toggleSidebar()}
|
||||||
>
|
>
|
||||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
|
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -12,9 +12,10 @@ import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
|||||||
import { CustomSelect, Tooltip } from "@plane/ui";
|
import { CustomSelect, Tooltip } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { TUserWorkspaceRole } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
import { TUserWorkspaceRole } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
member: {
|
member: {
|
||||||
@ -40,7 +41,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
|
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
|
||||||
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings },
|
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const isAdmin = currentWorkspaceRole === 20;
|
const isAdmin = currentWorkspaceRole === 20;
|
||||||
// states
|
// states
|
||||||
@ -48,49 +49,69 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
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 () => {
|
const handleRemoveMember = async () => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
if (member.member)
|
await removeMember(workspaceSlug.toString(), member.id).catch((err) =>
|
||||||
await removeMember(workspaceSlug.toString(), member.id)
|
|
||||||
.then(() => {
|
|
||||||
const memberId = member.memberId;
|
|
||||||
|
|
||||||
if (memberId === currentUser?.id && currentUserSettings) {
|
|
||||||
if (currentUserSettings.workspace?.invites > 0) router.push("/invitations");
|
|
||||||
else router.push("/create-workspace");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: err?.error || "Something went wrong",
|
message: err?.error || "Something went wrong. Please try again.",
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
else
|
};
|
||||||
|
|
||||||
|
const handleRemoveInvitation = async () => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id)
|
await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id)
|
||||||
.then(() => {
|
.then(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Member removed successfully",
|
message: "Invitation removed successfully.",
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
)
|
||||||
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: err?.error || "Something went wrong",
|
message: err?.error || "Something went wrong. Please try again.",
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
)
|
||||||
|
.finally(() =>
|
||||||
mutate(`WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`, (prevData: any) => {
|
mutate(`WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`, (prevData: any) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
return prevData.filter((item: any) => item.id !== member.id);
|
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;
|
if (!currentWorkspaceMemberInfo) return null;
|
||||||
@ -101,7 +122,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
isOpen={removeMemberModal}
|
isOpen={removeMemberModal}
|
||||||
onClose={() => setRemoveMemberModal(false)}
|
onClose={() => setRemoveMemberModal(false)}
|
||||||
data={member}
|
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="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">
|
<div className="flex items-center gap-x-4 gap-y-2">
|
||||||
|
@ -33,11 +33,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea
|
|||||||
const displayName = member.display_name.toLowerCase();
|
const displayName = member.display_name.toLowerCase();
|
||||||
const fullName = `${member.first_name} ${member.last_name}`.toLowerCase();
|
const fullName = `${member.first_name} ${member.last_name}`.toLowerCase();
|
||||||
|
|
||||||
return (
|
return `${email}${displayName}${fullName}`.includes(searchQuery.toLowerCase());
|
||||||
displayName.includes(searchQuery.toLowerCase()) ||
|
|
||||||
fullName.includes(searchQuery.toLowerCase()) ||
|
|
||||||
email?.includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -61,7 +57,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea
|
|||||||
? searchedMembers?.map((member) => <WorkspaceMembersListItem key={member.id} member={member} />)
|
? searchedMembers?.map((member) => <WorkspaceMembersListItem key={member.id} member={member} />)
|
||||||
: null}
|
: null}
|
||||||
{searchedMembers?.length === 0 && (
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -42,7 +42,7 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
|
|||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
icon: 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">
|
<Menu as="div" className="relative col-span-4 text-left flex-grow h-full truncate">
|
||||||
{({ open }) => (
|
{({ 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
|
<div
|
||||||
className={`flex items-center justify-between gap-x-2 rounded p-1 truncate ${
|
className={`flex items-center justify-between gap-x-2 rounded p-1 truncate ${
|
||||||
sidebarCollapsed ? "justify-center" : ""
|
sidebarCollapsed ? "justify-center" : ""
|
||||||
@ -131,7 +131,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
|
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<ChevronDown
|
<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" : ""
|
open ? "rotate-180" : ""
|
||||||
} text-custom-sidebar-text-400 duration-300`}
|
} text-custom-sidebar-text-400 duration-300`}
|
||||||
/>
|
/>
|
||||||
|
@ -13,7 +13,6 @@ export const InstanceAdminSidebar: FC<IInstanceAdminSidebar> = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="app-sidebar"
|
|
||||||
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
||||||
themStore?.sidebarCollapsed ? "" : "md:w-[280px]"
|
themStore?.sidebarCollapsed ? "" : "md:w-[280px]"
|
||||||
} ${themStore?.sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
} ${themStore?.sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
||||||
|
@ -19,7 +19,6 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="app-sidebar"
|
|
||||||
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
||||||
themStore?.sidebarCollapsed ? "" : "md:w-[280px]"
|
themStore?.sidebarCollapsed ? "" : "md:w-[280px]"
|
||||||
} ${themStore?.sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
} ${themStore?.sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
export * from "./profile";
|
||||||
export * from "./project";
|
export * from "./project";
|
||||||
export * from "./workspace";
|
export * from "./workspace";
|
||||||
|
3
web/layouts/settings-layout/profile/index.ts
Normal file
3
web/layouts/settings-layout/profile/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./layout";
|
||||||
|
export * from "./settings-sidebar";
|
||||||
|
export * from "./sidebar";
|
35
web/layouts/settings-layout/profile/layout.tsx
Normal file
35
web/layouts/settings-layout/profile/layout.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { FC, ReactNode } from "react";
|
||||||
|
// layout
|
||||||
|
import { UserAuthWrapper } from "layouts/auth-layout";
|
||||||
|
// components
|
||||||
|
import { ProfileLayoutSidebar, ProfileSettingsSidebar } from "layouts/settings-layout";
|
||||||
|
import { CommandPalette } from "components/command-palette";
|
||||||
|
|
||||||
|
interface IProfileSettingsLayout {
|
||||||
|
children: ReactNode;
|
||||||
|
header: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileSettingsLayout: FC<IProfileSettingsLayout> = (props) => {
|
||||||
|
const { children, header } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CommandPalette />
|
||||||
|
<UserAuthWrapper>
|
||||||
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
|
<ProfileLayoutSidebar />
|
||||||
|
<main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100">
|
||||||
|
{header}
|
||||||
|
<div className="flex gap-2 h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||||
|
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||||
|
<ProfileSettingsSidebar />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</UserAuthWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
48
web/layouts/settings-layout/profile/settings-sidebar.tsx
Normal file
48
web/layouts/settings-layout/profile/settings-sidebar.tsx
Normal file
@ -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 (
|
||||||
|
<div className="flex flex-col gap-2 w-80 px-5">
|
||||||
|
<span className="text-xs text-custom-sidebar-text-400 font-semibold">My Account</span>
|
||||||
|
<div className="flex flex-col gap-1 w-full">
|
||||||
|
{PROFILE_LINKS.map((link) => (
|
||||||
|
<Link key={link.href} href={link.href}>
|
||||||
|
<a>
|
||||||
|
<div
|
||||||
|
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||||
|
router.asPath === link.href
|
||||||
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
119
web/layouts/settings-layout/profile/sidebar.tsx
Normal file
119
web/layouts/settings-layout/profile/sidebar.tsx
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
||||||
|
sidebarCollapsed ? "" : "md:w-[280px]"
|
||||||
|
} ${sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<div className="w-full cursor-pointer space-y-1 p-4 flex-shrink-0">
|
||||||
|
{SIDEBAR_LINKS.map((link) => (
|
||||||
|
<Link key={link.key} href={link.href}>
|
||||||
|
<a className="block w-full">
|
||||||
|
<Tooltip tooltipContent={link.name} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
||||||
|
<div
|
||||||
|
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${
|
||||||
|
sidebarCollapsed ? "justify-center" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{<link.Icon className="h-4 w-4" />}
|
||||||
|
{!sidebarCollapsed && link.name}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{workspaces && workspaces.length > 0 && (
|
||||||
|
<div className="flex flex-col px-4 flex-shrink-0">
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<div className="rounded text-custom-sidebar-text-400 px-1.5 text-sm font-semibold">Your workspaces</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{workspaces.map((workspace) => (
|
||||||
|
<Link
|
||||||
|
key={workspace.id}
|
||||||
|
href={`/${workspace.slug}`}
|
||||||
|
className={`flex items-center flex-grow truncate cursor-pointer select-none text-left text-sm font-medium ${
|
||||||
|
sidebarCollapsed ? "justify-center" : `justify-between`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className={`flex items-center flex-grow w-full truncate gap-x-2 px-2 py-1 hover:bg-custom-sidebar-background-80 rounded-md ${
|
||||||
|
sidebarCollapsed ? "justify-center" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`relative flex h-6 w-6 items-center justify-center p-2 text-xs uppercase flex-shrink-0 ${
|
||||||
|
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{workspace?.logo && workspace.logo !== "" ? (
|
||||||
|
<img
|
||||||
|
src={workspace.logo}
|
||||||
|
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
||||||
|
alt="Workspace Logo"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
workspace?.name?.charAt(0) ?? "..."
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<p className="truncate text-custom-sidebar-text-200 text-sm">{workspace.name}</p>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-grow flex items-end px-4 py-2">
|
||||||
|
<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={() => toggleSidebar()}
|
||||||
|
>
|
||||||
|
<MoveLeft className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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 ml-auto ${
|
||||||
|
sidebarCollapsed ? "w-full" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => toggleSidebar()}
|
||||||
|
>
|
||||||
|
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${sidebarCollapsed ? "rotate-180" : ""}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -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 (
|
return (
|
||||||
<div className="flex flex-col gap-6 w-80 px-5">
|
<div className="flex flex-col gap-6 w-80 px-5">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@ -114,26 +89,6 @@ export const WorkspaceSettingsSidebar = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-xs text-custom-sidebar-text-400 font-semibold">My Account</span>
|
|
||||||
<div className="flex flex-col gap-1 w-full">
|
|
||||||
{profileLinks.map((link) => (
|
|
||||||
<Link key={link.href} href={link.href}>
|
|
||||||
<a>
|
|
||||||
<div
|
|
||||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
|
||||||
(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
|
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import useSWR from "swr";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
|
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) {
|
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader title="Assigned" />}>
|
<AppLayout header={<UserProfileHeader />}>
|
||||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
|
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) {
|
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader title="Created" />}>
|
<AppLayout header={<UserProfileHeader />}>
|
||||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ import useSWR from "swr";
|
|||||||
import { UserService } from "services/user.service";
|
import { UserService } from "services/user.service";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
import {
|
import {
|
||||||
@ -56,7 +56,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
|
|||||||
|
|
||||||
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader title="Summary" />}>
|
<AppLayout header={<UserProfileHeader />}>
|
||||||
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
// components
|
// components
|
||||||
import { UserProfileHeader } from "components/headers";
|
import { UserProfileHeader } from "components/headers";
|
||||||
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
|
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) {
|
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader title="Subscribed" />}>
|
<AppLayout header={<UserProfileHeader />}>
|
||||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -14,10 +14,11 @@ import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/w
|
|||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { trackEvent } from "helpers/event-tracker.helper";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
import { IWorkspaceBulkInviteFormData } from "types";
|
import { IWorkspaceBulkInviteFormData } from "types";
|
||||||
import { trackEvent } from "helpers/event-tracker.helper";
|
|
||||||
|
|
||||||
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -36,7 +37,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
|
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
|
||||||
.then(async (res) => {
|
.then(async () => {
|
||||||
setInviteModal(false);
|
setInviteModal(false);
|
||||||
trackEvent("WORKSPACE_USER_INVITE");
|
trackEvent("WORKSPACE_USER_INVITE");
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -67,14 +68,14 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||||
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-100">
|
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-100">
|
||||||
<h4 className="text-xl font-medium">Members</h4>
|
<h4 className="text-xl font-medium">Members</h4>
|
||||||
<div className="flex gap-1 items-center justify-start ml-auto text-custom-text-400 rounded-md px-2.5 py-1.5 border border-custom-border-200 bg-custom-background-100">
|
<div className="flex items-center gap-1.5 ml-auto rounded-md px-2.5 py-1.5 border border-custom-border-200 bg-custom-background-100">
|
||||||
<Search className="h-3.5 w-3.5" />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" />
|
||||||
<input
|
<input
|
||||||
className="max-w-[234px] w-full border-none bg-transparent text-sm focus:outline-none"
|
className="max-w-[234px] w-full border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
|
||||||
placeholder="Search"
|
placeholder="Search..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
autoFocus={true}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
|
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
|
||||||
|
@ -5,12 +5,11 @@ import Link from "next/link";
|
|||||||
// services
|
// services
|
||||||
import { UserService } from "services/user.service";
|
import { UserService } from "services/user.service";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||||
import { AppLayout } from "layouts/app-layout";
|
|
||||||
// components
|
// components
|
||||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||||
import { RichReadOnlyEditor } from "@plane/rich-text-editor";
|
import { RichReadOnlyEditor } from "@plane/rich-text-editor";
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { ProfileSettingsHeader } from "components/headers";
|
||||||
// icons
|
// icons
|
||||||
import { History, MessageSquare } from "lucide-react";
|
import { History, MessageSquare } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
@ -28,10 +27,7 @@ const ProfileActivityPage: NextPageWithLayout = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { data: userActivity } = useSWR(
|
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
|
||||||
workspaceSlug ? USER_ACTIVITY : null,
|
|
||||||
workspaceSlug ? () => userService.getUserWorkspaceActivity(workspaceSlug.toString()) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -197,11 +193,7 @@ const ProfileActivityPage: NextPageWithLayout = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ProfileActivityPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileActivityPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <ProfileSettingsLayout header={<ProfileSettingsHeader title="Activity" />}>{page}</ProfileSettingsLayout>;
|
||||||
<AppLayout header={<WorkspaceSettingHeader title="My Profile Activity" />}>
|
|
||||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfileActivityPage;
|
export default ProfileActivityPage;
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useState, ReactElement } from "react";
|
import React, { useEffect, useState, ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
import { UserService } from "services/user.service";
|
import { UserService } from "services/user.service";
|
||||||
@ -9,15 +8,15 @@ import { UserService } from "services/user.service";
|
|||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
|
||||||
// components
|
// components
|
||||||
import { ImagePickerPopover, ImageUploadModal } from "components/core";
|
import { ImagePickerPopover, ImageUploadModal } from "components/core";
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { ProfileSettingsHeader } from "components/headers";
|
||||||
|
import { DeactivateAccountModal } from "components/account";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui";
|
import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { User2, UserCircle2 } from "lucide-react";
|
import { ChevronDown, User2 } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import type { IUser } from "types";
|
import type { IUser } from "types";
|
||||||
import type { NextPageWithLayout } from "types/app";
|
import type { NextPageWithLayout } from "types/app";
|
||||||
@ -30,6 +29,7 @@ const defaultValues: Partial<IUser> = {
|
|||||||
cover_image: "",
|
cover_image: "",
|
||||||
first_name: "",
|
first_name: "",
|
||||||
last_name: "",
|
last_name: "",
|
||||||
|
display_name: "",
|
||||||
email: "",
|
email: "",
|
||||||
role: "Product / Project Manager",
|
role: "Product / Project Manager",
|
||||||
user_timezone: "Asia/Kolkata",
|
user_timezone: "Asia/Kolkata",
|
||||||
@ -38,12 +38,11 @@ const defaultValues: Partial<IUser> = {
|
|||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
const ProfilePage: NextPageWithLayout = () => {
|
const ProfileSettingsPage: NextPageWithLayout = () => {
|
||||||
const [isRemoving, setIsRemoving] = useState(false);
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||||
// router
|
const [deactivateAccountModal, setDeactivateAccountModal] = useState(false);
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -143,6 +142,13 @@ const ProfilePage: NextPageWithLayout = () => {
|
|||||||
content: timeZone.label,
|
content: timeZone.label,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (!myProfile)
|
||||||
|
return (
|
||||||
|
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ImageUploadModal
|
<ImageUploadModal
|
||||||
@ -158,9 +164,10 @@ const ProfilePage: NextPageWithLayout = () => {
|
|||||||
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
||||||
userImage
|
userImage
|
||||||
/>
|
/>
|
||||||
{myProfile ? (
|
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="h-full w-full">
|
<div className="h-full w-full flex flex-col py-9 pr-9 space-y-10 overflow-y-auto">
|
||||||
<div className={`flex flex-col gap-8 pr-9 py-9 w-full h-full overflow-y-auto`}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="flex flex-col gap-8 w-full">
|
||||||
<div className="relative h-44 w-full mt-6">
|
<div className="relative h-44 w-full mt-6">
|
||||||
<img
|
<img
|
||||||
src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||||
@ -194,14 +201,12 @@ const ProfilePage: NextPageWithLayout = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="cover_image"
|
name="cover_image"
|
||||||
render={() => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<ImagePickerPopover
|
<ImagePickerPopover
|
||||||
label={"Change cover"}
|
label={"Change cover"}
|
||||||
onChange={(imageUrl) => {
|
onChange={(imageUrl) => onChange(imageUrl)}
|
||||||
setValue("cover_image", imageUrl);
|
|
||||||
}}
|
|
||||||
control={control}
|
control={control}
|
||||||
value={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -216,14 +221,12 @@ const ProfilePage: NextPageWithLayout = () => {
|
|||||||
<span className="text-sm tracking-tight">{watch("email")}</span>
|
<span className="text-sm tracking-tight">{watch("email")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={`/${workspaceSlug}/profile/${myProfile.id}`}>
|
{/* <Link href={`/profile/${myProfile.id}`}>
|
||||||
<a className="flex item-center cursor-pointer gap-2 h-4 leading-4 text-sm text-custom-primary-100">
|
<a className="flex item-center gap-1 text-sm text-custom-primary-100 underline font-medium">
|
||||||
<span className="h-4 w-4">
|
<ExternalLink className="h-4 w-4" />
|
||||||
<UserCircle2 className="h-4 w-4" />
|
Activity Overview
|
||||||
</span>
|
|
||||||
View Profile
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
|
||||||
@ -387,21 +390,49 @@ const ProfilePage: NextPageWithLayout = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
<Disclosure as="div" className="border-t border-custom-border-100 px-8">
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
{({ open }) => (
|
||||||
<Spinner />
|
<>
|
||||||
|
<Disclosure.Button as="button" type="button" className="flex items-center justify-between w-full py-4">
|
||||||
|
<span className="text-lg tracking-tight">Deactivate Account</span>
|
||||||
|
{/* <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" /> */}
|
||||||
|
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
||||||
|
</Disclosure.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform opacity-0"
|
||||||
|
enterTo="transform opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform opacity-100"
|
||||||
|
leaveTo="transform opacity-0"
|
||||||
|
>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<span className="text-sm tracking-tight">
|
||||||
|
The danger zone of the profile page is a critical area that requires careful consideration and
|
||||||
|
attention. When deactivating an account, all of the data and resources within that account will be
|
||||||
|
permanently removed and cannot be recovered.
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
|
||||||
|
Deactivate account
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ProfilePage.getLayout = function getLayout(page: ReactElement) {
|
ProfileSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <ProfileSettingsLayout header={<ProfileSettingsHeader title="Settings" />}>{page}</ProfileSettingsLayout>;
|
||||||
<AppLayout header={<WorkspaceSettingHeader title="My Profile" />}>
|
|
||||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfilePage;
|
export default ProfileSettingsPage;
|
@ -5,11 +5,10 @@ import { useTheme } from "next-themes";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
|
||||||
// components
|
// components
|
||||||
import { CustomThemeSelector, ThemeSwitch } from "components/core";
|
import { CustomThemeSelector, ThemeSwitch } from "components/core";
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { ProfileSettingsHeader } from "components/headers";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
@ -76,11 +75,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ProfilePreferencesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfilePreferencesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <ProfileSettingsLayout header={<ProfileSettingsHeader title="Preferences" />}>{page}</ProfileSettingsLayout>;
|
||||||
<AppLayout header={<WorkspaceSettingHeader title="My Profile Preferences" />}>
|
|
||||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfilePreferencesPage;
|
export default ProfilePreferencesPage;
|
@ -22,8 +22,8 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
|||||||
import { IUser, TOnboardingSteps } from "types";
|
import { IUser, TOnboardingSteps } from "types";
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import { Menu, Popover, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import DeleteAccountModal from "components/account/delete-account-modal";
|
import { DeactivateAccountModal } from "components/account";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
@ -101,12 +101,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteAccountModal
|
<DeactivateAccountModal isOpen={showDeleteModal} onClose={() => setShowDeleteModal(false)} />
|
||||||
isOpen={showDeleteModal}
|
|
||||||
onClose={() => {
|
|
||||||
setShowDeleteModal(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{user && step !== null ? (
|
{user && step !== null ? (
|
||||||
<div className={`bg-onboarding-gradient-100 h-full flex flex-col fixed w-full`}>
|
<div className={`bg-onboarding-gradient-100 h-full flex flex-col fixed w-full`}>
|
||||||
<div className="sm:pt-14 sm:pb-8 py-10 px-4 sm:px-7 md:px-14 lg:pl-28 lg:pr-24 flex items-center">
|
<div className="sm:pt-14 sm:pb-8 py-10 px-4 sm:px-7 md:px-14 lg:pl-28 lg:pr-24 flex items-center">
|
||||||
|
@ -61,28 +61,4 @@ export class ProjectMemberService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchProjectInvitations(workspaceSlug: string, projectId: string): Promise<IProjectMemberInvitation[]> {
|
|
||||||
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<any> {
|
|
||||||
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<any> {
|
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/${invitationId}/`)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -68,22 +68,6 @@ export class ProjectService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinProject(workspaceSlug: string, project_ids: string[]): Promise<any> {
|
|
||||||
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<any> {
|
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setProjectView(
|
async setProjectView(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -96,8 +96,8 @@ export class UserService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserWorkspaceActivity(workspaceSlug: string): Promise<IUserActivityResponse> {
|
async getUserActivity(): Promise<IUserActivityResponse> {
|
||||||
return this.get(`/api/users/workspaces/${workspaceSlug}/activities/`)
|
return this.get(`/api/users/me/activities/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
@ -185,12 +185,35 @@ export class UserService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAccount(): Promise<void> {
|
async deactivateAccount() {
|
||||||
return this.delete("/api/users/me/")
|
return this.delete(`/api/users/me/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.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<any> {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ import { IProjectMember } from "types";
|
|||||||
import { ProjectMemberService } from "services/project";
|
import { ProjectMemberService } from "services/project";
|
||||||
|
|
||||||
export interface IProjectMemberStore {
|
export interface IProjectMemberStore {
|
||||||
|
// states
|
||||||
|
error: any | null;
|
||||||
|
|
||||||
// observables
|
// observables
|
||||||
members: {
|
members: {
|
||||||
[projectId: string]: IProjectMember[] | null; // project_id: members
|
[projectId: string]: IProjectMember[] | null; // project_id: members
|
||||||
@ -28,6 +31,10 @@ export interface IProjectMemberStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectMemberStore implements IProjectMemberStore {
|
export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
|
// states
|
||||||
|
error: any | null = null;
|
||||||
|
|
||||||
|
// observables
|
||||||
members: {
|
members: {
|
||||||
[projectId: string]: IProjectMember[]; // projectId: members
|
[projectId: string]: IProjectMember[]; // projectId: members
|
||||||
} = {};
|
} = {};
|
||||||
@ -117,6 +124,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
*/
|
*/
|
||||||
removeMemberFromProject = async (workspaceSlug: string, projectId: string, memberId: string) => {
|
removeMemberFromProject = async (workspaceSlug: string, projectId: string, memberId: string) => {
|
||||||
const originalMembers = this.projectMembers || [];
|
const originalMembers = this.projectMembers || [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.members = {
|
this.members = {
|
||||||
@ -124,17 +132,20 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
[projectId]: this.projectMembers?.filter((member) => member.id !== memberId) || [],
|
[projectId]: this.projectMembers?.filter((member) => member.id !== memberId) || [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberId);
|
await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberId);
|
||||||
await this.fetchProjectMembers(workspaceSlug, projectId);
|
await this.fetchProjectMembers(workspaceSlug, projectId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to delete project from project store");
|
|
||||||
// revert back to original members in case of error
|
// revert back to original members in case of error
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
this.members = {
|
this.members = {
|
||||||
...this.members,
|
...this.members,
|
||||||
[projectId]: originalMembers,
|
[projectId]: originalMembers,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -39,8 +39,6 @@ export interface IProjectStore {
|
|||||||
orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number;
|
orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number;
|
||||||
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
|
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
|
||||||
|
|
||||||
joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<void>;
|
|
||||||
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
|
||||||
createProject: (workspaceSlug: string, data: any) => Promise<any>;
|
createProject: (workspaceSlug: string, data: any) => Promise<any>;
|
||||||
updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<any>;
|
updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<any>;
|
||||||
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
@ -100,7 +98,6 @@ export class ProjectStore implements IProjectStore {
|
|||||||
updateProjectView: action,
|
updateProjectView: action,
|
||||||
createProject: action,
|
createProject: action,
|
||||||
updateProject: action,
|
updateProject: action,
|
||||||
leaveProject: action,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
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) => {
|
createProject = async (workspaceSlug: string, data: any) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.projectService.createProject(workspaceSlug, data);
|
const response = await this.projectService.createProject(workspaceSlug, data);
|
||||||
|
@ -53,6 +53,13 @@ export interface IUserStore {
|
|||||||
updateTourCompleted: () => Promise<void>;
|
updateTourCompleted: () => Promise<void>;
|
||||||
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser>;
|
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser>;
|
||||||
updateCurrentUserTheme: (theme: string) => Promise<IUser>;
|
updateCurrentUserTheme: (theme: string) => Promise<IUser>;
|
||||||
|
|
||||||
|
deactivateAccount: () => Promise<void>;
|
||||||
|
|
||||||
|
leaveWorkspace: (workspaceSlug: string) => Promise<void>;
|
||||||
|
|
||||||
|
joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<any>;
|
||||||
|
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserStore implements IUserStore {
|
class UserStore implements IUserStore {
|
||||||
@ -110,6 +117,10 @@ class UserStore implements IUserStore {
|
|||||||
updateTourCompleted: action,
|
updateTourCompleted: action,
|
||||||
updateCurrentUser: action,
|
updateCurrentUser: action,
|
||||||
updateCurrentUserTheme: action,
|
updateCurrentUserTheme: action,
|
||||||
|
deactivateAccount: action,
|
||||||
|
leaveWorkspace: action,
|
||||||
|
joinProject: action,
|
||||||
|
leaveProject: action,
|
||||||
// computed
|
// computed
|
||||||
currentProjectMemberInfo: computed,
|
currentProjectMemberInfo: computed,
|
||||||
currentWorkspaceMemberInfo: computed,
|
currentWorkspaceMemberInfo: computed,
|
||||||
@ -179,7 +190,7 @@ class UserStore implements IUserStore {
|
|||||||
if (response) {
|
if (response) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.isUserInstanceAdmin = response.is_instance_admin;
|
this.isUserInstanceAdmin = response.is_instance_admin;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
return response.is_instance_admin;
|
return response.is_instance_admin;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -350,6 +361,67 @@ class UserStore implements IUserStore {
|
|||||||
throw error;
|
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;
|
export default UserStore;
|
||||||
|
@ -259,18 +259,14 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
|||||||
const index = members.findIndex((m) => m.id === memberId);
|
const index = members.findIndex((m) => m.id === memberId);
|
||||||
members.splice(index, 1);
|
members.splice(index, 1);
|
||||||
|
|
||||||
// optimistic update
|
|
||||||
runInAction(() => {
|
|
||||||
this.members = {
|
|
||||||
...this.members,
|
|
||||||
[workspaceSlug]: members,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
this.members = {
|
||||||
|
...this.members,
|
||||||
|
[workspaceSlug]: members,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId);
|
await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId);
|
||||||
|
1
web/types/issues.d.ts
vendored
1
web/types/issues.d.ts
vendored
@ -209,6 +209,7 @@ export interface IIssueActivity {
|
|||||||
updated_by: string;
|
updated_by: string;
|
||||||
verb: string;
|
verb: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
|
workspace_detail?: IWorkspaceLite;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueComment extends IIssueActivity {
|
export interface IIssueComment extends IIssueActivity {
|
||||||
|
Loading…
Reference in New Issue
Block a user