Merge branch 'chore-admin-file-structure' of github.com:makeplane/plane into chore-admin-file-structure

This commit is contained in:
pablohashescobar 2024-05-07 17:36:00 +05:30
commit 6c09e6dbde
59 changed files with 458 additions and 606 deletions

View File

@ -7,7 +7,7 @@ const nextConfig = {
images: {
unoptimized: true,
},
basePath: process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? "/god-mode" : "",
basePath: "/god-mode",
};
module.exports = nextConfig;

View File

@ -10,7 +10,8 @@ import { EAuthModes, EAuthSteps, ForgotPasswordPopover, PasswordStrengthMeter }
import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { getPasswordStrength } from "@/helpers/password.helper";
import { useMobxStore } from "@/lib/mobx/store-provider";
// hooks
import { useInstance } from "@/hooks/store";
import { AuthService } from "@/services/authentication.service";
type Props = {
@ -42,9 +43,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// hooks
const {
instanceStore: { instance },
} = useMobxStore();
const { instance } = useInstance();
// router
const router = useRouter();
const { next_path } = router.query;

View File

@ -4,8 +4,8 @@ import { observer } from "mobx-react-lite";
import { IEmailCheckData } from "@plane/types";
import { EmailForm, UniqueCodeForm, PasswordForm, OAuthOptions, TermsAndConditions } from "@/components/accounts";
// hooks
import { useInstance } from "@/hooks/store";
import useToast from "@/hooks/use-toast";
import { useMobxStore } from "@/lib/mobx/store-provider";
// services
import { AuthService } from "@/services/authentication.service";
@ -60,9 +60,7 @@ export const AuthRoot = observer(() => {
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState("");
// hooks
const {
instanceStore: { instance },
} = useMobxStore();
const { instance } = useInstance();
// derived values
const isSmtpConfigured = instance?.config?.is_smtp_configured;

View File

@ -2,13 +2,11 @@ import { observer } from "mobx-react-lite";
// components
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/accounts";
// hooks
import { useMobxStore } from "@/lib/mobx/store-provider";
import { useInstance } from "@/hooks/store";
export const OAuthOptions: React.FC = observer(() => {
// hooks
const {
instanceStore: { instance },
} = useMobxStore();
const { instance } = useInstance();
return (
<>

View File

@ -6,7 +6,7 @@ import { Transition, Dialog } from "@headlessui/react";
// hooks
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useMobxStore } from "@/lib/mobx/store-provider";
import { useInstance } from "@/hooks/store";
// services
import fileService from "@/services/file.service";
@ -27,9 +27,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
const [image, setImage] = useState<File | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);
// store hooks
const {
instanceStore: { instance },
} = useMobxStore();
const { instance } = useInstance();
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);

View File

@ -1,17 +1,14 @@
import Image from "next/image";
// mobx
import { useMobxStore } from "@/lib/mobx/store-provider";
// hooks
import { useUser } from "@/hooks/store";
// assets
import PlaneLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import UserLoggedInImage from "public/user-logged-in.svg";
export const UserLoggedIn = () => {
const { user: userStore } = useMobxStore();
const user = userStore.currentUser;
const { data: user } = useUser();
if (!user) return null;
return (
<div className="flex h-screen w-screen flex-col">
<div className="relative flex w-full items-center justify-between gap-4 border-b border-custom-border-200 px-6 py-5">

View File

@ -10,14 +10,7 @@ import instanceNotReady from "public/instance/plane-instance-not-ready.webp";
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
type TInstanceNotReady = {
isGodModeEnabled: boolean;
handleGodModeStateChange?: (state: boolean) => void;
};
export const InstanceNotReady: FC<TInstanceNotReady> = () => {
// const { isGodModeEnabled, handleGodModeStateChange } = props;
export const InstanceNotReady: FC = () => {
const { resolvedTheme } = useTheme();
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;

View File

@ -10,7 +10,7 @@ import { useMobxStore } from "@/lib/mobx/store-provider";
// components
// interfaces
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { IIssue } from "types/issue";
export const IssueKanBanBlock = observer(({ issue }: { issue: IIssue }) => {

View File

@ -7,7 +7,7 @@ import { issueGroupFilter } from "@/constants/data";
// ui
// mobx hook
import { useMobxStore } from "@/lib/mobx/store-provider";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { IIssueState } from "types/issue";
export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => {

View File

@ -10,7 +10,7 @@ import { Icon } from "@/components/ui";
// interfaces
// mobx hook
import { useMobxStore } from "@/lib/mobx/store-provider";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { IIssueState, IIssue } from "types/issue";
export const IssueKanbanView = observer(() => {

View File

@ -9,7 +9,7 @@ import { IssueBlockState } from "@/components/issues/board-views/block-state";
// mobx hook
import { useMobxStore } from "@/lib/mobx/store-provider";
// interfaces
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { IIssue } from "types/issue";
// store

View File

@ -7,7 +7,7 @@ import { StateGroupIcon } from "@plane/ui";
import { issueGroupFilter } from "@/constants/data";
// mobx hook
import { useMobxStore } from "@/lib/mobx/store-provider";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { IIssueState } from "types/issue";
export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {

View File

@ -6,7 +6,7 @@ import { IssueListHeader } from "@/components/issues/board-views/list/header";
// mobx hook
import { useMobxStore } from "@/lib/mobx/store-provider";
// store
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { IIssueState, IIssue } from "types/issue";
export const IssueListView = observer(() => {

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
// store
import { useMobxStore } from "@/lib/mobx/store-provider";
import { IIssueFilterOptions } from "@/store/issues/types";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { AppliedFiltersList } from "./filters-list";
export const IssueAppliedFilters: FC = observer(() => {

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
import { useMobxStore } from "@/lib/mobx/store-provider";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/store/issues/helpers";
import { IIssueFilterOptions } from "@/store/issues/types";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { FiltersDropdown } from "./helpers/dropdown";
import { FilterSelection } from "./selection";
// types

View File

@ -7,21 +7,21 @@ import { Briefcase } from "lucide-react";
import { Avatar, Button } from "@plane/ui";
import { ProjectLogo } from "@/components/common";
import { IssueFiltersDropdown } from "@/components/issues/filters";
// ui
// lib
// hooks
import { useUser } from "@/hooks/store";
import { useMobxStore } from "@/lib/mobx/store-provider";
// store
import { RootStore } from "@/store/root";
import { TIssueBoardKeys } from "types/issue";
import { RootStore } from "@/store/root.store";
import { TIssueBoardKeys } from "@/types/issue";
import { NavbarIssueBoardView } from "./issue-board-view";
import { NavbarTheme } from "./theme";
const IssueNavbar = observer(() => {
const {
project: projectStore,
user: userStore,
issuesFilter: { updateFilters },
}: RootStore = useMobxStore();
const { data: user } = useUser();
// router
const router = useRouter();
const { workspace_slug, project_slug, board, peekId, states, priorities, labels } = router.query as {
@ -34,8 +34,6 @@ const IssueNavbar = observer(() => {
labels: string;
};
const user = userStore?.currentUser;
useEffect(() => {
if (workspace_slug && project_slug) {
projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString());

View File

@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { issueViews } from "@/constants/data";
// mobx
import { useMobxStore } from "@/lib/mobx/store-provider";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { TIssueBoardKeys } from "types/issue";
export const NavbarIssueBoardView = observer(() => {

View File

@ -6,6 +6,7 @@ import { useForm, Controller } from "react-hook-form";
import { EditorRefApi } from "@plane/lite-text-editor";
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
// hooks
import { useUser } from "@/hooks/store";
import useToast from "@/hooks/use-toast";
// lib
import { useMobxStore } from "@/lib/mobx/store-provider";
@ -29,7 +30,8 @@ export const AddComment: React.FC<Props> = observer(() => {
const { workspace_slug, project_slug } = router.query;
// store hooks
const { project } = useMobxStore();
const { user: userStore, issueDetails: issueDetailStore } = useMobxStore();
const { issueDetails: issueDetailStore } = useMobxStore();
const { data: currentUser } = useUser();
// derived values
const workspaceId = project.workspace?.id;
const issueId = issueDetailStore.peekId;
@ -62,6 +64,7 @@ export const AddComment: React.FC<Props> = observer(() => {
);
};
// TODO: on click if he user is not logged in redirect to login page
return (
<div>
<div className="issue-comments-section">
@ -70,7 +73,9 @@ export const AddComment: React.FC<Props> = observer(() => {
control={control}
render={({ field: { value, onChange } }) => (
<LiteTextEditor
onEnterKeyPress={(e) => userStore.requiredLogin(() => handleSubmit(onSubmit)(e))}
onEnterKeyPress={(e) => {
if (currentUser) handleSubmit(onSubmit)(e);
}}
workspaceId={workspaceId as string}
workspaceSlug={workspace_slug as string}
ref={editorRef}

View File

@ -9,10 +9,12 @@ import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor";
import { CommentReactions } from "@/components/issues/peek-overview";
// helpers
import { timeAgo } from "@/helpers/date-time.helper";
// hooks
import { useUser } from "@/hooks/store";
// mobx store
import { useMobxStore } from "@/lib/mobx/store-provider";
// store
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
// types
import { Comment } from "@/types/issue";
@ -27,7 +29,8 @@ export const CommentCard: React.FC<Props> = observer((props) => {
const workspaceId = project.workspace?.id;
// store
const { user: userStore, issueDetails: issueDetailStore } = useMobxStore();
const { issueDetails: issueDetailStore } = useMobxStore();
const { data: currentUser } = useUser();
// states
const [isEditing, setIsEditing] = useState(false);
// refs
@ -139,7 +142,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
</div>
</div>
{userStore?.currentUser?.id === comment?.actor_detail?.id && (
{currentUser?.id === comment?.actor_detail?.id && (
<Menu as="div" className="relative w-min text-left">
<Menu.Button
type="button"

View File

@ -8,6 +8,8 @@ import { Tooltip } from "@plane/ui";
import { ReactionSelector } from "@/components/ui";
// helpers
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
// hooks
import { useUser } from "@/hooks/store";
import { useMobxStore } from "@/lib/mobx/store-provider";
type Props = {
@ -20,12 +22,11 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspace_slug } = router.query;
const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore();
// hooks
const { issueDetails: issueDetailsStore } = useMobxStore();
const { data: user } = useUser();
const peekId = issueDetailsStore.peekId;
const user = userStore.currentUser;
const commentReactions = peekId
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
: [];
@ -64,13 +65,13 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
else handleAddReaction(reactionHex);
};
// TODO: on onclick redirect to login page if the user is not logged in
return (
<div className="mt-2 flex items-center gap-1.5">
<ReactionSelector
onSelect={(value) => {
userStore.requiredLogin(() => {
handleReactionClick(value);
});
if (user) handleReactionClick(value);
// userStore.requiredLogin(() => {});
}}
position="top"
selected={userReactions?.map((r) => r.reaction)}
@ -98,14 +99,11 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
<button
type="button"
onClick={() => {
userStore.requiredLogin(() => {
handleReactionClick(reaction);
});
if (user) handleReactionClick(reaction);
// userStore.requiredLogin(() => {});
}}
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
commentReactions?.some(
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
)
commentReactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
? "bg-custom-primary-100/10"
: "bg-custom-background-80"
}`}
@ -113,9 +111,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
<span>{renderEmoji(reaction)}</span>
<span
className={
commentReactions?.some(
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
)
commentReactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
? "text-custom-primary-100"
: ""
}

View File

@ -10,7 +10,7 @@ import { copyTextToClipboard } from "@/helpers/string.helper";
// store
import { useMobxStore } from "@/lib/mobx/store-provider";
import { IPeekMode } from "@/store/issue_details";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
// lib
import useToast from "hooks/use-toast";
// types

View File

@ -1,19 +1,16 @@
import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
// mobx
// lib
import { Button } from "@plane/ui";
import { CommentCard, AddComment } from "@/components/issues/peek-overview";
import { Icon } from "@/components/ui";
// hooks
import { useUser } from "@/hooks/store";
import { useMobxStore } from "@/lib/mobx/store-provider";
// components
// ui
// types
import { IIssue } from "types/issue";
import { IIssue } from "@/types/issue";
type Props = {
issueDetails: IIssue;
@ -24,11 +21,8 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer(() => {
const router = useRouter();
const { workspace_slug } = router.query;
// store
const {
issueDetails: issueDetailStore,
project: projectStore,
user: { currentUser },
} = useMobxStore();
const { issueDetails: issueDetailStore, project: projectStore } = useMobxStore();
const { data: currentUser } = useUser();
const comments = issueDetailStore.details[issueDetailStore.peekId || ""]?.comments || [];
return (

View File

@ -5,6 +5,8 @@ import { useRouter } from "next/router";
import { Tooltip } from "@plane/ui";
import { ReactionSelector } from "@/components/ui";
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
// hooks
import { useUser } from "@/hooks/store";
import { useMobxStore } from "@/lib/mobx/store-provider";
// helpers
// components
@ -14,9 +16,9 @@ export const IssueEmojiReactions: React.FC = observer(() => {
const router = useRouter();
const { workspace_slug, project_slug } = router.query;
// store
const { user: userStore, issueDetails: issueDetailsStore } = useMobxStore();
const { issueDetails: issueDetailsStore } = useMobxStore();
const { data: user, fetchCurrentUser } = useUser();
const user = userStore?.currentUser;
const issueId = issueDetailsStore.peekId;
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
const groupedReactions = groupReactions(reactions, "reaction");
@ -44,16 +46,16 @@ export const IssueEmojiReactions: React.FC = observer(() => {
useEffect(() => {
if (user) return;
userStore.fetchCurrentUser();
}, [user, userStore]);
fetchCurrentUser();
}, [user, fetchCurrentUser]);
// TODO: on onclick of reaction, if user not logged in redirect to login page
return (
<>
<ReactionSelector
onSelect={(value) => {
userStore.requiredLogin(() => {
handleReactionClick(value);
});
if (user) handleReactionClick(value);
// userStore.requiredLogin(() => {});
}}
selected={userReactions?.map((r) => r.reaction)}
size="md"
@ -80,9 +82,8 @@ export const IssueEmojiReactions: React.FC = observer(() => {
<button
type="button"
onClick={() => {
userStore.requiredLogin(() => {
handleReactionClick(reaction);
});
if (user) handleReactionClick(reaction);
// userStore.requiredLogin(() => {});
}}
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
reactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)

View File

@ -6,6 +6,8 @@ import { useRouter } from "next/router";
// mobx
// lib
import { Tooltip } from "@plane/ui";
// hooks
import { useUser } from "@/hooks/store";
import { useMobxStore } from "@/lib/mobx/store-provider";
// ui
@ -16,9 +18,9 @@ export const IssueVotes: React.FC = observer(() => {
const { workspace_slug, project_slug } = router.query;
const { user: userStore, issueDetails: issueDetailsStore } = useMobxStore();
const { issueDetails: issueDetailsStore } = useMobxStore();
const { data: user, fetchCurrentUser } = useUser();
const user = userStore?.currentUser;
const issueId = issueDetailsStore.peekId;
const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
@ -49,8 +51,8 @@ export const IssueVotes: React.FC = observer(() => {
useEffect(() => {
if (user) return;
userStore.fetchCurrentUser();
}, [user, userStore]);
fetchCurrentUser();
}, [user, fetchCurrentUser]);
const VOTES_LIMIT = 1000;
@ -78,9 +80,8 @@ export const IssueVotes: React.FC = observer(() => {
type="button"
disabled={isSubmitting}
onClick={(e) => {
userStore.requiredLogin(() => {
handleVote(e, 1);
});
if (user) handleVote(e, 1);
// userStore.requiredLogin(() => {});
}}
className={`flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 focus:outline-none ${
isUpVotedByUser ? "border-custom-primary-200 text-custom-primary-200" : "border-custom-border-300"
@ -113,9 +114,8 @@ export const IssueVotes: React.FC = observer(() => {
type="button"
disabled={isSubmitting}
onClick={(e) => {
userStore.requiredLogin(() => {
handleVote(e, -1);
});
if (user) handleVote(e, -1);
// userStore.requiredLogin(() => {});
}}
className={`flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 focus:outline-none ${
isDownVotedByUser ? "border-red-600 text-red-600" : "border-custom-border-300"

View File

@ -1,4 +1,3 @@
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
// ui
@ -7,9 +6,8 @@ import useSWR from "swr";
import { Spinner } from "@plane/ui";
// components
import { AuthRoot, UserLoggedIn } from "@/components/accounts";
// mobx
import useAuthRedirection from "@/hooks/use-auth-redirection";
import { useMobxStore } from "@/lib/mobx/store-provider";
// hooks
import { useUser } from "@/hooks/store";
// images
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
@ -19,11 +17,7 @@ export const AuthView = observer(() => {
// hooks
const { resolvedTheme } = useTheme();
// store
const {
user: { currentUser, fetchCurrentUser, loader },
} = useMobxStore();
// sign in redirection hook
const { isRedirecting, handleRedirection } = useAuthRedirection();
const { data: currentUser, fetchCurrentUser, isLoading } = useUser();
// fetching user information
useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
@ -31,13 +25,9 @@ export const AuthView = observer(() => {
revalidateOnFocus: false,
});
useEffect(() => {
handleRedirection();
}, [handleRedirection]);
return (
<>
{loader || isRedirecting ? (
{isLoading ? (
<div className="relative flex h-screen w-screen items-center justify-center">
<Spinner />
</div>

View File

@ -11,8 +11,9 @@ import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadshee
import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
// mobx store
import { useUser } from "@/hooks/store";
import { useMobxStore } from "@/lib/mobx/store-provider";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
// assets
import SomethingWentWrongImage from "public/something-went-wrong.svg";
@ -20,18 +21,14 @@ export const ProjectDetailsView = observer(() => {
const router = useRouter();
const { workspace_slug, project_slug, states, labels, priorities, peekId } = router.query;
const {
issue: issueStore,
project: projectStore,
issueDetails: issueDetailStore,
user: userStore,
}: RootStore = useMobxStore();
const { issue: issueStore, project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
const { data: currentUser, fetchCurrentUser } = useUser();
useEffect(() => {
if (!userStore.currentUser) {
userStore.fetchCurrentUser();
if (!currentUser) {
fetchCurrentUser();
}
}, [userStore]);
}, [currentUser, fetchCurrentUser]);
useEffect(() => {
if (workspace_slug && project_slug) {

View File

@ -0,0 +1,2 @@
export * from "./use-instance";
export * from "./user";

View File

@ -0,0 +1,10 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/lib/store-context";
import { IInstanceStore } from "@/store/instance.store";
export const useInstance = (): IInstanceStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useInstance must be used within StoreProvider");
return context.instance;
};

View File

@ -0,0 +1,2 @@
export * from "./use-user";
export * from "./use-user-profile";

View File

@ -0,0 +1,10 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/lib/store-context";
import { IProfileStore } from "@/store/user/profile.store";
export const useUserProfile = (): IProfileStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.profile;
};

View File

@ -0,0 +1,10 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/lib/store-context";
import { IUserStore } from "@/store/user/index.store";
export const useUser = (): IUserStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUser must be used within StoreProvider");
return context.user;
};

View File

@ -1,94 +0,0 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
// types
import { TUserProfile } from "@plane/types";
// mobx store
import { useMobxStore } from "@/lib/mobx/store-provider";
type UseAuthRedirectionProps = {
error: any | null;
isRedirecting: boolean;
handleRedirection: () => Promise<void>;
};
const useAuthRedirection = (): UseAuthRedirectionProps => {
// states
const [isRedirecting, setIsRedirecting] = useState(false);
const [error, setError] = useState<any | null>(null);
// router
const router = useRouter();
const { next_path } = router.query;
// mobx store
const {
profile: { fetchUserProfile },
} = useMobxStore();
const isValidURL = (url: string): boolean => {
const disallowedSchemes = /^(https?|ftp):\/\//i;
return !disallowedSchemes.test(url);
};
const getAuthRedirectionUrl = useCallback(
async (profile: TUserProfile | undefined) => {
try {
if (!profile) return "/";
const isOnboard = profile.onboarding_step?.profile_complete;
if (isOnboard) {
// if next_path is provided, redirect the user to that url
if (next_path) {
if (isValidURL(next_path.toString())) {
return next_path.toString();
} else {
return "/";
}
}
} else {
// if the user profile is not complete, redirect them to the onboarding page to complete their profile and then redirect them to the next path
if (next_path) return `/onboarding?next_path=${next_path}`;
else return "/onboarding";
}
return "/";
} catch {
setIsRedirecting(false);
console.error("Error in handleSignInRedirection:", error);
setError(error);
}
},
[next_path]
);
const updateUserProfileInfo = useCallback(async () => {
setIsRedirecting(true);
await fetchUserProfile()
.then(async (profile) => {
if (profile)
await getAuthRedirectionUrl(profile)
.then((url: string | undefined) => {
if (url) {
router.push(url);
}
if (!url || url === "/") setIsRedirecting(false);
})
.catch((err) => {
setError(err);
setIsRedirecting(false);
});
})
.catch((err) => {
setError(err);
setIsRedirecting(false);
});
}, [fetchUserProfile, getAuthRedirectionUrl]);
return {
error,
isRedirecting,
handleRedirection: updateUserProfileInfo,
};
};
export default useAuthRedirection;

View File

@ -1,5 +1,5 @@
import { useMobxStore } from "@/lib/mobx/store-provider";
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
const useEditorSuggestions = () => {
const { mentionsStore }: RootStore = useMobxStore();

View File

@ -1,4 +1,4 @@
import { FC, ReactNode, useState } from "react";
import { FC, ReactNode } from "react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// ui
@ -6,7 +6,7 @@ import { Spinner } from "@plane/ui";
// components
import { InstanceNotReady } from "@/components/instance";
// hooks
import { useMobxStore } from "@/lib/mobx/store-provider";
import { useInstance } from "@/hooks/store";
type TInstanceLayout = {
children: ReactNode;
@ -15,18 +15,12 @@ type TInstanceLayout = {
const InstanceLayout: FC<TInstanceLayout> = observer((props) => {
const { children } = props;
// store
const {
instanceStore: { isLoading, instance, error, fetchInstanceInfo },
} = useMobxStore();
// states
const [isGodModeEnabled, setIsGodModeEnabled] = useState(false);
const handleGodModeStateChange = (state: boolean) => setIsGodModeEnabled(state);
const { isLoading, instance, fetchInstanceInfo } = useInstance();
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
revalidateOnFocus: false,
});
// loading state
if (isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
@ -34,21 +28,7 @@ const InstanceLayout: FC<TInstanceLayout> = observer((props) => {
</div>
);
// something went wrong while in the request
if (error && error?.status === "error")
return (
<div className="relative flex h-screen w-screen items-center justify-center">
Something went wrong. please try again later
</div>
);
// checking if the instance is activated or not
if (error && !error?.data?.is_activated) return <InstanceNotReady isGodModeEnabled={false} />;
// instance is not ready and setup is not done
if (instance?.instance?.is_setup_done === false)
// if (isGodModeEnabled) return <MiniGodModeForm />;
return <InstanceNotReady isGodModeEnabled handleGodModeStateChange={handleGodModeStateChange} />;
if (instance?.instance?.is_setup_done === false) return <InstanceNotReady />;
return <>{children}</>;
});

View File

@ -2,7 +2,7 @@
import { createContext, useContext } from "react";
// mobx store
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
let rootStore: RootStore = new RootStore();

View File

@ -0,0 +1,19 @@
import { ReactElement, createContext } from "react";
// mobx store
import { RootStore } from "@/store/root.store";
let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
const initializeStore = () => {
const singletonRootStore = rootStore ?? new RootStore();
if (typeof window === "undefined") return singletonRootStore;
if (!rootStore) rootStore = singletonRootStore;
return singletonRootStore;
};
export const StoreProvider = ({ children }: { children: ReactElement }) => {
const store = initializeStore();
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};

View File

@ -13,7 +13,7 @@ const nextConfig = {
},
];
},
basePath: process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? "/spaces" : "",
basePath: "/spaces",
reactStrictMode: false,
swcMinify: true,
images: {
@ -29,7 +29,8 @@ const nextConfig = {
};
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
module.exports = withSentryConfig(nextConfig,
module.exports = withSentryConfig(
nextConfig,
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN },
{ hideSourceMaps: true }
);

View File

@ -8,7 +8,7 @@ import { Avatar } from "@plane/ui";
// components
import { OnBoardingForm } from "@/components/accounts/onboarding-form";
// mobx
import { useMobxStore } from "@/lib/mobx/store-provider";
import { useUser, useUserProfile } from "@/hooks/store";
// assets
import ProfileSetupDark from "public/onboarding/profile-setup-dark.svg";
import ProfileSetup from "public/onboarding/profile-setup.svg";
@ -22,12 +22,10 @@ const OnBoardingPage = observer(() => {
// hooks
const { resolvedTheme } = useTheme();
const {
user: userStore,
profile: { currentUserProfile, updateUserProfile },
} = useMobxStore();
const user = userStore?.currentUser;
const { data: user } = useUser();
const { data: currentUserProfile, updateUserProfile } = useUserProfile();
if (!user) {
router.push("/");
return <></>;

View File

@ -1,108 +1,52 @@
import axios from "axios";
import Cookies from "js-cookie";
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, { AxiosInstance } from "axios";
abstract class APIService {
protected baseURL: string;
protected headers: any = {};
private axiosInstance: AxiosInstance;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
setCSRFToken(token: string) {
Cookies.set("csrf_token", token, { expires: 30 });
}
getCSRFToken() {
return Cookies.get("csrf_token");
}
setRefreshToken(token: string) {
Cookies.set("refresh_token", token, { expires: 30 });
}
getRefreshToken() {
return Cookies.get("refresh_token");
}
purgeRefreshToken() {
Cookies.remove("refresh_token", { path: "/" });
}
setAccessToken(token: string) {
Cookies.set("access_token", token, { expires: 30 });
}
getAccessToken() {
return Cookies.get("access_token");
}
purgeAccessToken() {
Cookies.remove("access_token", { path: "/" });
}
getHeaders() {
return {
Authorization: `Bearer ${this.getAccessToken()}`,
};
}
get(url: string, config = {}): Promise<any> {
return axios({
method: "get",
url: this.baseURL + url,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
this.axiosInstance = axios.create({
baseURL,
withCredentials: true,
});
this.setupInterceptors();
}
post(url: string, data = {}, config = {}): Promise<any> {
return axios({
method: "post",
url: this.baseURL + url,
data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
withCredentials: true,
});
private setupInterceptors() {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) window.location.href = "/space/login";
return Promise.reject(error.response?.data ?? error);
}
);
}
put(url: string, data = {}, config = {}): Promise<any> {
return axios({
method: "put",
url: this.baseURL + url,
data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
withCredentials: true,
});
get(url: string, params = {}) {
return this.axiosInstance.get(url, { params });
}
patch(url: string, data = {}, config = {}): Promise<any> {
return axios({
method: "patch",
url: this.baseURL + url,
data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
withCredentials: true,
});
post(url: string, data: any, config = {}) {
return this.axiosInstance.post(url, data, config);
}
delete(url: string, data?: any, config = {}): Promise<any> {
return axios({
method: "delete",
url: this.baseURL + url,
data: data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
withCredentials: true,
});
put(url: string, data: any, config = {}) {
return this.axiosInstance.put(url, data, config);
}
patch(url: string, data: any, config = {}) {
return this.axiosInstance.patch(url, data, config);
}
delete(url: string, data?: any, config = {}) {
return this.axiosInstance.delete(url, { data, ...config });
}
request(config = {}) {
return axios(config);
return this.axiosInstance(config);
}
}

View File

@ -42,17 +42,5 @@ export class AuthService extends APIService {
});
}
async signOut() {
return this.post("/api/sign-out/", { refresh_token: this.getRefreshToken() })
.then((response) => {
this.purgeAccessToken();
this.purgeRefreshToken();
return response?.data;
})
.catch((error) => {
this.purgeAccessToken();
this.purgeRefreshToken();
throw error?.response?.data;
});
}
async signOut() {}
}

View File

@ -43,7 +43,6 @@ class FileService extends APIService {
this.cancelSource = axios.CancelToken.source();
return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, {
headers: {
...this.getHeaders(),
"Content-Type": "multipart/form-data",
},
cancelToken: this.cancelSource.token,
@ -117,7 +116,6 @@ class FileService extends APIService {
async restoreImage(assetUrlWithWorkspaceId: string): Promise<any> {
return this.post(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/restore/`, {
headers: this.getHeaders(),
"Content-Type": "application/json",
})
.then((response) => response?.status)
@ -140,7 +138,6 @@ class FileService extends APIService {
async uploadUserFile(file: FormData): Promise<any> {
return this.post(`/api/users/file-assets/`, file, {
headers: {
...this.getHeaders(),
"Content-Type": "multipart/form-data",
},
})

View File

@ -3,24 +3,13 @@ import type { IInstance } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
import APIService from "@/services/api.service";
import APIService from "@/services/api.service";
export class InstanceService extends APIService {
constructor() {
super(API_BASE_URL);
}
async requestCSRFToken(): Promise<{ csrf_token: string }> {
return this.get("/auth/get-csrf-token/")
.then((response) => {
this.setCSRFToken(response.data.csrf_token);
return response.data;
})
.catch((error) => {
throw error;
});
}
async getInstanceInfo(): Promise<IInstance> {
return this.get("/api/instances/")
.then((response) => response.data)
@ -28,21 +17,4 @@ export class InstanceService extends APIService {
throw error;
});
}
async createInstanceAdmin(data: FormData): Promise<void> {
return this.post("/api/instances/admins/sign-in/", {
headers: {
"Content-Type": "multipart/form-data",
"X-CSRFToken": this.getCSRFToken(),
},
data,
})
.then((response) => {
console.log("response.data", response.data);
response.data;
})
.catch((error) => {
throw error;
});
}
}

View File

@ -3,7 +3,8 @@ import { observable, action, makeObservable, runInAction } from "mobx";
import { IInstance } from "@plane/types";
// services
import { InstanceService } from "@/services/instance.service";
import { RootStore } from "./root";
// store types
import { RootStore } from "@/store/root.store";
type TError = {
status: string;
@ -27,12 +28,10 @@ export class InstanceStore implements IInstanceStore {
isLoading: boolean = true;
instance: IInstance | undefined = undefined;
error: TError | undefined = undefined;
// root store
rootStore: RootStore;
// services
instanceService;
constructor(_rootStore: any) {
constructor(private store: RootStore) {
makeObservable(this, {
// observable
isLoading: observable.ref,
@ -41,7 +40,6 @@ export class InstanceStore implements IInstanceStore {
// actions
fetchInstanceInfo: action,
});
this.rootStore = _rootStore;
// services
this.instanceService = new InstanceService();
}
@ -51,33 +49,13 @@ export class InstanceStore implements IInstanceStore {
*/
fetchInstanceInfo = async () => {
try {
runInAction(() => {
this.isLoading = true;
this.error = undefined;
});
this.isLoading = true;
this.error = undefined;
const instance = await this.instanceService.getInstanceInfo();
const isInstanceNotSetup = (instance: IInstance) => "is_activated" in instance && "is_setup_done" in instance;
if (isInstanceNotSetup(instance)) {
runInAction(() => {
this.isLoading = false;
this.error = {
status: "success",
message: "Instance is not created in the backend",
data: {
is_activated: instance?.instance?.is_activated,
is_setup_done: instance?.instance?.is_setup_done,
},
};
});
} else {
runInAction(() => {
this.isLoading = false;
this.instance = instance;
});
}
runInAction(() => {
this.isLoading = false;
this.instance = instance;
});
} catch (error) {
runInAction(() => {
this.isLoading = false;

View File

@ -2,7 +2,7 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"
// services
import IssueService from "@/services/issue.service";
// store
import { RootStore } from "./root";
import { RootStore } from "./root.store";
// types
// import { IssueDetailType, TIssueBoardKeys } from "types/issue";
import { IIssue, IIssueState, IIssueLabel } from "types/issue";

View File

@ -1,10 +1,11 @@
import { makeObservable, observable, action, runInAction } from "mobx";
import { v4 as uuidv4 } from "uuid";
// store
import { RootStore } from "./root";
// services
import IssueService from "@/services/issue.service";
import { IIssue, IVote } from "types/issue";
// store types
import { RootStore } from "@/store/root.store";
// types
import { IIssue, IVote } from "@/types/issue";
export type IPeekMode = "side" | "modal" | "full";
@ -330,7 +331,7 @@ class IssueDetailStore implements IIssueDetailStore {
removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
try {
const newReactions = this.details[issueId].reactions.filter(
(_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.currentUser?.id)
(_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.data?.id)
);
runInAction(() => {
@ -361,7 +362,7 @@ class IssueDetailStore implements IIssueDetailStore {
addIssueVote = async (workspaceSlug: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => {
const newVote: IVote = {
actor: this.rootStore.user.currentUser?.id ?? "",
actor: this.rootStore.user.data?.id ?? "",
actor_detail: this.rootStore.user.currentActor,
issue: issueId,
project: projectId,
@ -369,7 +370,7 @@ class IssueDetailStore implements IIssueDetailStore {
vote: data.vote,
};
const filteredVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.currentUser?.id);
const filteredVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.data?.id);
try {
runInAction(() => {
@ -400,7 +401,7 @@ class IssueDetailStore implements IIssueDetailStore {
};
removeIssueVote = async (workspaceSlug: string, projectId: string, issueId: string) => {
const newVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.currentUser?.id);
const newVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.data?.id);
try {
runInAction(() => {

View File

@ -1,5 +1,5 @@
// types
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
export interface IIssueFilterBaseStore {
// helper methods

View File

@ -1,6 +1,6 @@
import { action, makeObservable, observable, runInAction, computed } from "mobx";
// types
import { RootStore } from "@/store/root";
import { RootStore } from "@/store/root.store";
import { IIssueFilterOptions, TIssueParams } from "./types";
import { handleIssueQueryParamsByLayout } from "./helpers";
import { IssueFilterBaseStore } from "./base-issue-filter.store";

View File

@ -1,6 +1,6 @@
import { IMentionHighlight } from "@plane/lite-text-editor";
import { RootStore } from "./root";
import { computed, makeObservable } from "mobx";
import { IMentionHighlight } from "@plane/lite-text-editor";
import { RootStore } from "./root.store";
export interface IMentionsStore {
// mentionSuggestions: IMentionSuggestion[];
@ -37,7 +37,7 @@ export class MentionsStore implements IMentionsStore {
// }
get mentionHighlights() {
const user = this.rootStore.user.currentUser;
const user = this.rootStore.user.data;
return user ? [user.id] : [];
}
}

View File

@ -2,9 +2,9 @@
import { observable, action, makeObservable, runInAction } from "mobx";
// service
import ProjectService from "@/services/project.service";
import { TIssueBoardKeys } from "types/issue";
// types
import { IWorkspace, IProject, IProjectSettings } from "types/project";
import { TIssueBoardKeys } from "@/types/issue";
import { IWorkspace, IProject, IProjectSettings } from "@/types/project";
export interface IProjectStore {
loader: boolean;
@ -18,7 +18,7 @@ export interface IProjectStore {
setActiveBoard: (value: TIssueBoardKeys) => void;
}
class ProjectStore implements IProjectStore {
export class ProjectStore implements IProjectStore {
loader: boolean = false;
error: any | null = null;
// data
@ -61,15 +61,15 @@ class ProjectStore implements IProjectStore {
const response = await this.projectService.getProjectSettings(workspace_slug, project_slug);
if (response) {
const _project: IProject = { ...response?.project_details };
const _workspace: IWorkspace = { ...response?.workspace_detail };
const _viewOptions = { ...response?.views };
const _deploySettings = { ...response };
const currentProject: IProject = { ...response?.project_details };
const currentWorkspace: IWorkspace = { ...response?.workspace_detail };
const currentViewOptions = { ...response?.views };
const currentDeploySettings = { ...response };
runInAction(() => {
this.project = _project;
this.workspace = _workspace;
this.viewOptions = _viewOptions;
this.deploySettings = _deploySettings;
this.project = currentProject;
this.workspace = currentWorkspace;
this.viewOptions = currentViewOptions;
this.deploySettings = currentDeploySettings;
this.loader = false;
});
}
@ -85,5 +85,3 @@ class ProjectStore implements IProjectStore {
this.activeBoard = boardValue;
};
}
export default ProjectStore;

View File

@ -1,35 +1,52 @@
// mobx lite
import { enableStaticRendering } from "mobx-react-lite";
// store imports
import { InstanceStore } from "./instance.store";
import { IInstanceStore, InstanceStore } from "@/store/instance.store";
import { IProjectStore, ProjectStore } from "@/store/project";
import { IUserStore, UserStore } from "@/store/user/index.store";
import { IProfileStore, ProfileStore } from "@/store/user/profile.store";
import IssueStore, { IIssueStore } from "./issue";
import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store";
import { IMentionsStore, MentionsStore } from "./mentions.store";
import ProfileStore from "./profile";
import ProjectStore, { IProjectStore } from "./project";
import UserStore from "./user";
enableStaticRendering(typeof window === "undefined");
export class RootStore {
instanceStore: InstanceStore;
user: UserStore;
profile: ProfileStore;
instance: IInstanceStore;
user: IUserStore;
profile: IProfileStore;
project: IProjectStore;
issue: IIssueStore;
issueDetails: IIssueDetailStore;
project: IProjectStore;
mentionsStore: IMentionsStore;
issuesFilter: IIssuesFilterStore;
constructor() {
this.instanceStore = new InstanceStore(this);
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.profile = new ProfileStore(this);
this.issue = new IssueStore(this);
this.project = new ProjectStore(this);
this.issue = new IssueStore(this);
this.issueDetails = new IssueDetailStore(this);
this.mentionsStore = new MentionsStore(this);
this.issuesFilter = new IssuesFilterStore(this);
}
resetOnSignOut = () => {
localStorage.setItem("theme", "system");
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.profile = new ProfileStore(this);
this.project = new ProjectStore(this);
this.issue = new IssueStore(this);
this.issueDetails = new IssueDetailStore(this);
this.mentionsStore = new MentionsStore(this);
this.issuesFilter = new IssuesFilterStore(this);
};
}

View File

@ -1,119 +0,0 @@
// mobx
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// types
import { IUser } from "@plane/types";
// service
import { UserService } from "@/services/user.service";
export interface IUserStore {
loader: boolean;
error: any | null;
currentUser: any | null;
fetchCurrentUser: () => Promise<IUser | undefined>;
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser>;
currentActor: () => any;
}
class UserStore implements IUserStore {
loader: boolean = false;
error: any | null = null;
currentUser: IUser | null = null;
// root store
rootStore;
// service
userService;
constructor(_rootStore: any) {
makeObservable(this, {
// observable
loader: observable.ref,
error: observable.ref,
currentUser: observable.ref,
// actions
setCurrentUser: action,
// computed
currentActor: computed,
});
this.rootStore = _rootStore;
this.userService = new UserService();
}
setCurrentUser = (user: any) => {
runInAction(() => {
this.currentUser = { ...user };
});
};
get currentActor(): any {
return {
avatar: this.currentUser?.avatar,
display_name: this.currentUser?.display_name,
first_name: this.currentUser?.first_name,
id: this.currentUser?.id,
is_bot: false,
last_name: this.currentUser?.last_name,
};
}
/**
*
* @param callback
* @description A wrapper function to check user authentication; it redirects to the login page if not authenticated, otherwise, it executes a callback.
* @example this.requiredLogin(() => { // do something });
*/
requiredLogin = (callback: () => void) => {
if (this.currentUser) {
callback();
return;
}
const currentPath = window.location.pathname + window.location.search;
this.fetchCurrentUser()
.then(() => {
if (!this.currentUser) window.location.href = `/?next_path=${currentPath}`;
else callback();
})
.catch(() => (window.location.href = `/?next_path=${currentPath}`));
};
fetchCurrentUser = async () => {
try {
this.loader = true;
this.error = null;
const response = await this.userService.currentUser();
if (response)
runInAction(() => {
this.loader = false;
this.currentUser = response;
});
return response;
} catch (error) {
console.error("Failed to fetch current user", error);
this.loader = false;
this.error = error;
}
};
/**
* Updates the current user
* @param data
* @returns Promise<IUser>
*/
updateCurrentUser = async (data: Partial<IUser>) => {
try {
const user = await this.userService.updateUser(data);
runInAction(() => {
this.currentUser = user;
});
return user;
} catch (error) {
throw error;
}
};
}
export default UserStore;

View File

@ -0,0 +1,164 @@
import set from "lodash/set";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
// types
import { IUser } from "@plane/types";
// helpers
// import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { AuthService } from "@/services/authentication.service";
import { UserService } from "@/services/user.service";
// stores
import { RootStore } from "@/store/root.store";
import { ProfileStore, IProfileStore } from "@/store/user/profile.store";
import { ActorDetail } from "@/types/issue";
type TUserErrorStatus = {
status: string;
message: string;
};
export interface IUserStore {
// observables
isAuthenticated: boolean;
isLoading: boolean;
error: TUserErrorStatus | undefined;
data: IUser | undefined;
// store observables
userProfile: IProfileStore;
// computed
currentActor: ActorDetail;
// actions
fetchCurrentUser: () => Promise<IUser | undefined>;
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser | undefined>;
signOut: () => Promise<void>;
}
export class UserStore implements IUserStore {
// observables
isAuthenticated: boolean = false;
isLoading: boolean = false;
error: TUserErrorStatus | undefined = undefined;
data: IUser | undefined = undefined;
// store observables
userProfile: IProfileStore;
// service
userService: UserService;
authService: AuthService;
constructor(private store: RootStore) {
// stores
this.userProfile = new ProfileStore(store);
// service
this.userService = new UserService();
this.authService = new AuthService();
// observables
makeObservable(this, {
// observables
isAuthenticated: observable.ref,
isLoading: observable.ref,
error: observable,
// model observables
data: observable,
userProfile: observable,
// computed
currentActor: computed,
// actions
fetchCurrentUser: action,
updateCurrentUser: action,
signOut: action,
});
}
// computed
get currentActor(): ActorDetail {
return {
id: this.data?.id,
first_name: this.data?.first_name,
last_name: this.data?.last_name,
display_name: this.data?.display_name,
avatar: this.data?.avatar || undefined,
is_bot: false,
};
}
// actions
/**
* @description fetches the current user
* @returns {Promise<IUser>}
*/
fetchCurrentUser = async (): Promise<IUser> => {
try {
runInAction(() => {
this.isLoading = true;
this.error = undefined;
});
const user = await this.userService.currentUser();
if (user && user?.id) {
await this.userProfile.fetchUserProfile();
runInAction(() => {
this.data = user;
this.isLoading = false;
this.isAuthenticated = true;
});
} else
runInAction(() => {
this.data = user;
this.isLoading = false;
this.isAuthenticated = false;
});
return user;
} catch (error) {
runInAction(() => {
this.isLoading = false;
this.isAuthenticated = false;
this.error = {
status: "user-fetch-error",
message: "Failed to fetch current user",
};
});
throw error;
}
};
/**
* @description updates the current user
* @param data
* @returns {Promise<IUser>}
*/
updateCurrentUser = async (data: Partial<IUser>): Promise<IUser> => {
const currentUserData = this.data;
try {
if (currentUserData) {
Object.keys(data).forEach((key: string) => {
const userKey: keyof IUser = key as keyof IUser;
if (this.data) set(this.data, userKey, data[userKey]);
});
}
const user = await this.userService.updateUser(data);
return user;
} catch (error) {
if (currentUserData) {
Object.keys(currentUserData).forEach((key: string) => {
const userKey: keyof IUser = key as keyof IUser;
if (this.data) set(this.data, userKey, currentUserData[userKey]);
});
}
runInAction(() => {
this.error = {
status: "user-update-error",
message: "Failed to update current user",
};
});
throw error;
}
};
/**
* @description signs out the current user
* @returns {Promise<void>}
*/
signOut = async (): Promise<void> => {
// await this.authService.signOut(API_BASE_URL);
// this.store.resetOnSignOut();
};
}

View File

@ -1,9 +1,10 @@
import set from "lodash/set";
import { action, makeObservable, observable, runInAction } from "mobx";
// services
import { TUserProfile } from "@plane/types";
import { UserService } from "services/user.service";
// types
import { RootStore } from "./root";
// services
import { UserService } from "@/services/user.service";
// store types
import { RootStore } from "@/store/root.store";
type TError = {
status: string;
@ -13,16 +14,17 @@ type TError = {
export interface IProfileStore {
// observables
isLoading: boolean;
currentUserProfile: TUserProfile;
error: TError | undefined;
data: TUserProfile;
// actions
fetchUserProfile: () => Promise<TUserProfile | undefined>;
updateUserProfile: (currentUserProfile: Partial<TUserProfile>) => Promise<void>;
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
}
class ProfileStore implements IProfileStore {
export class ProfileStore implements IProfileStore {
isLoading: boolean = false;
currentUserProfile: TUserProfile = {
error: TError | undefined = undefined;
data: TUserProfile = {
id: undefined,
user: undefined,
role: undefined,
@ -52,28 +54,29 @@ class ProfileStore implements IProfileStore {
created_at: "",
updated_at: "",
};
error: TError | undefined = undefined;
// root store
rootStore;
// services
userService: UserService;
constructor(_rootStore: RootStore) {
constructor(public store: RootStore) {
makeObservable(this, {
// observables
isLoading: observable.ref,
currentUserProfile: observable,
error: observable,
data: observable,
// actions
fetchUserProfile: action,
updateUserProfile: action,
});
this.rootStore = _rootStore;
// services
this.userService = new UserService();
}
// actions
/**
* @description fetches user profile information
* @returns {Promise<TUserProfile | undefined>}
*/
fetchUserProfile = async () => {
try {
runInAction(() => {
@ -83,45 +86,49 @@ class ProfileStore implements IProfileStore {
const userProfile = await this.userService.getCurrentUserProfile();
runInAction(() => {
this.isLoading = false;
this.currentUserProfile = userProfile;
this.data = userProfile;
});
return userProfile;
} catch (error) {
console.log("Failed to fetch profile details");
runInAction(() => {
this.isLoading = true;
this.isLoading = false;
this.error = {
status: "error",
message: "Failed to fetch instance info",
status: "user-profile-fetch-error",
message: "Failed to fetch user profile",
};
});
throw error;
}
};
updateUserProfile = async (currentUserProfile: Partial<TUserProfile>) => {
/**
* @description updated the user profile information
* @param {Partial<TUserProfile>} data
* @returns {Promise<TUserProfile | undefined>}
*/
updateUserProfile = async (data: Partial<TUserProfile>) => {
const currentUserProfileData = this.data;
try {
runInAction(() => {
this.isLoading = true;
this.error = undefined;
});
const userProfile = await this.userService.updateCurrentUserProfile(currentUserProfile);
runInAction(() => {
this.isLoading = false;
this.currentUserProfile = userProfile;
});
if (currentUserProfileData) {
Object.keys(data).forEach((key: string) => {
const userKey: keyof TUserProfile = key as keyof TUserProfile;
if (this.data) set(this.data, userKey, data[userKey]);
});
}
const userProfile = await this.userService.updateCurrentUserProfile(data);
return userProfile;
} catch (error) {
console.log("Failed to fetch profile details");
if (currentUserProfileData) {
Object.keys(currentUserProfileData).forEach((key: string) => {
const userKey: keyof TUserProfile = key as keyof TUserProfile;
if (this.data) set(this.data, userKey, currentUserProfileData[userKey]);
});
}
runInAction(() => {
this.isLoading = true;
this.error = {
status: "error",
message: "Failed to fetch instance info",
status: "user-profile-update-error",
message: "Failed to update user profile",
};
});
}
};
}
export default ProfileStore;