diff --git a/web/app/onboarding/page.tsx b/web/app/onboarding/page.tsx index ffc198698..7f314a08d 100644 --- a/web/app/onboarding/page.tsx +++ b/web/app/onboarding/page.tsx @@ -5,6 +5,8 @@ import { observer } from "mobx-react"; import useSWR from "swr"; // types import { TOnboardingSteps, TUserProfile } from "@plane/types"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { LogoSpinner } from "@/components/common"; import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding"; @@ -14,8 +16,7 @@ import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys"; // helpers import { EPageTypes } from "@/helpers/authentication.helper"; // hooks -import { useUser, useWorkspace, useUserProfile, useEventTracker, useUserSettings } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; +import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store"; // wrappers import { AuthenticationWrapper } from "@/lib/wrappers"; import { WorkspaceService } from "@/plane-web/services"; @@ -33,24 +34,33 @@ const OnboardingPage = observer(() => { // states const [step, setStep] = useState(null); const [totalSteps, setTotalSteps] = useState(null); - // router - const router = useAppRouter(); // store hooks const { captureEvent } = useEventTracker(); const { isLoading: userLoader, data: user, updateCurrentUser } = useUser(); - const { data: profile, updateUserOnBoard, updateUserProfile } = useUserProfile(); - const { data: currentUserSettings } = useUserSettings(); + const { data: profile, updateUserProfile, finishUserOnboarding } = useUserProfile(); const { workspaces, fetchWorkspaces } = useWorkspace(); // computed values const workspacesList = Object.values(workspaces ?? {}); // fetching workspaces list - const { isLoading: workspaceListLoader } = useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), { - shouldRetryOnError: false, - }); + const { isLoading: workspaceListLoader } = useSWR( + USER_WORKSPACES_LIST, + () => { + user?.id && fetchWorkspaces(); + }, + { + shouldRetryOnError: false, + } + ); // fetching user workspace invitations - const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS_LIST", () => - workspaceService.userWorkspaceInvitations() + const { data: invitations } = useSWR( + "USER_WORKSPACE_INVITATIONS_LIST", + () => { + user?.id && workspaceService.userWorkspaceInvitations(); + }, + { + shouldRetryOnError: false, + } ); // handle step change const stepChange = async (steps: Partial) => { @@ -66,45 +76,11 @@ const OnboardingPage = observer(() => { await updateUserProfile(payload); }; - const getWorkspaceRedirectionUrl = (): string => { - let redirectionRoute = "/profile"; - - // validate the last and fallback workspace_slug - const currentWorkspaceSlug = - currentUserSettings?.workspace?.last_workspace_slug || - currentUserSettings?.workspace?.fallback_workspace_slug || - undefined; - - if (currentWorkspaceSlug) { - const isCurrentWorkspaceValid = Object.values(workspaces || {}).findIndex( - (workspace) => workspace.slug === currentWorkspaceSlug - ); - if (isCurrentWorkspaceValid >= 0) { - redirectionRoute = `/${currentWorkspaceSlug}`; - } - } - - return redirectionRoute; - }; - // complete onboarding const finishOnboarding = async () => { - if (!user || !workspaces) return; + if (!user) return; - const firstWorkspace = Object.values(workspaces ?? {})?.[0]; - - await Promise.all([ - updateUserProfile({ - onboarding_step: { - profile_complete: true, - workspace_join: true, - workspace_create: true, - workspace_invite: true, - }, - last_workspace_id: firstWorkspace?.id, - }), - updateUserOnBoard(), - ]) + await finishUserOnboarding() .then(() => { captureEvent(USER_ONBOARDING_COMPLETED, { // user_role: user.role, @@ -114,10 +90,12 @@ const OnboardingPage = observer(() => { }); }) .catch(() => { - console.log("Failed to update onboarding status"); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Failed", + message: "Failed to finish onboarding, Please try again later.", + }); }); - - router.replace(`${getWorkspaceRedirectionUrl()}`); }; useEffect(() => { diff --git a/web/core/components/onboarding/invitations.tsx b/web/core/components/onboarding/invitations.tsx index 3c9d57e20..92bcade23 100644 --- a/web/core/components/onboarding/invitations.tsx +++ b/web/core/components/onboarding/invitations.tsx @@ -14,7 +14,7 @@ import { ROLE } from "@/constants/workspace"; import { truncateText } from "@/helpers/string.helper"; import { getUserRole } from "@/helpers/user.helper"; // hooks -import { useEventTracker, useWorkspace } from "@/hooks/store"; +import { useEventTracker, useUserSettings, useWorkspace } from "@/hooks/store"; // services import { WorkspaceService } from "@/plane-web/services"; @@ -32,6 +32,7 @@ export const Invitations: React.FC = (props) => { // store hooks const { captureEvent } = useEventTracker(); const { fetchWorkspaces } = useWorkspace(); + const { fetchCurrentUserSettings } = useUserSettings(); const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations()); @@ -61,6 +62,7 @@ export const Invitations: React.FC = (props) => { element: "Workspace invitations page", }); await fetchWorkspaces(); + await fetchCurrentUserSettings(); await handleNextStep(); } catch (error) { console.error(error); diff --git a/web/core/lib/wrappers/authentication-wrapper.tsx b/web/core/lib/wrappers/authentication-wrapper.tsx index 34c493b46..21690cf15 100644 --- a/web/core/lib/wrappers/authentication-wrapper.tsx +++ b/web/core/lib/wrappers/authentication-wrapper.tsx @@ -103,7 +103,7 @@ export const AuthenticationWrapper: FC = observer((props } else { if (currentUser && currentUserProfile?.id && isUserOnboard) { const currentRedirectRoute = getWorkspaceRedirectionUrl(); - router.push(currentRedirectRoute); + router.replace(currentRedirectRoute); return <>; } else return <>{children}; } diff --git a/web/core/services/api.service.ts b/web/core/services/api.service.ts index 0886fe9b0..30c646f59 100644 --- a/web/core/services/api.service.ts +++ b/web/core/services/api.service.ts @@ -20,7 +20,8 @@ export abstract class APIService { (response) => response, (error) => { if (error.response && error.response.status === 401) { - window.location.reload(); + const currentPath = window.location.pathname; + window.location.replace(`/${currentPath ? `?next_path=${currentPath}` : ``}`); } return Promise.reject(error); } diff --git a/web/core/store/user/profile.store.ts b/web/core/store/user/profile.store.ts index 79541469f..4847651f5 100644 --- a/web/core/store/user/profile.store.ts +++ b/web/core/store/user/profile.store.ts @@ -21,7 +21,7 @@ export interface IUserProfileStore { // actions fetchUserProfile: () => Promise; updateUserProfile: (data: Partial) => Promise; - updateUserOnBoard: () => Promise; + finishUserOnboarding: () => Promise; updateTourCompleted: () => Promise; updateUserTheme: (data: Partial) => Promise; } @@ -72,7 +72,6 @@ export class ProfileStore implements IUserProfileStore { // actions fetchUserProfile: action, updateUserProfile: action, - updateUserOnBoard: action, updateTourCompleted: action, updateUserTheme: action, }); @@ -80,12 +79,20 @@ export class ProfileStore implements IUserProfileStore { this.userService = new UserService(); } + // helper action + mutateUserProfile = (data: Partial) => { + if (!data) return + Object.entries(data).forEach(([key, value]) => { + if (key in this.data) set(this.data, key, value); + }) + } + // actions /** * @description fetches user profile information * @returns {Promise} */ - fetchUserProfile = async () => { + fetchUserProfile = async (): Promise => { try { runInAction(() => { this.isLoading = true; @@ -114,23 +121,17 @@ export class ProfileStore implements IUserProfileStore { * @param {Partial} data * @returns {Promise} */ - updateUserProfile = async (data: Partial) => { + updateUserProfile = async (data: Partial): Promise => { const currentUserProfileData = this.data; try { 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]); - }); + this.mutateUserProfile(data); } const userProfile = await this.userService.updateCurrentUserProfile(data); return userProfile; } catch (error) { 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]); - }); + this.mutateUserProfile(currentUserProfileData); } runInAction(() => { this.error = { @@ -142,27 +143,43 @@ export class ProfileStore implements IUserProfileStore { }; /** - * @description updates the user onboarding status - * @returns @returns {Promise} + * @description finishes the user onboarding + * @returns { void } */ - updateUserOnBoard = async () => { - const isUserProfileOnboard = this.data.is_onboarded || false; + finishUserOnboarding = async (): Promise => { try { - runInAction(() => set(this.data, ["is_onboarded"], true)); - const userProfile = await this.userService.updateUserOnBoard(); - return userProfile; - } catch (error) { + const firstWorkspace = Object.values(this.store.workspaceRoot.workspaces ?? {})?.[0]; + const dataToUpdate: Partial = { + onboarding_step: { + profile_complete: true, + workspace_join: true, + workspace_create: true, + workspace_invite: true, + }, + last_workspace_id: firstWorkspace?.id, + }; + + // update user onboarding steps + await this.userService.updateCurrentUserProfile(dataToUpdate); + + // update user onboarding status + await this.userService.updateUserOnBoard(); + + // update the user profile store runInAction(() => { - set(this.data, ["is_onboarded"], isUserProfileOnboard); - this.error = { - status: "user-profile-onboard-error", - message: "Failed to update user profile is_onboarded", - }; + this.mutateUserProfile({ ...dataToUpdate, is_onboarded: true }); }); + } catch (error) { + runInAction(() => { + this.error = { + status: "user-profile-onboard-finish-error", + message: "Failed to finish user onboarding", + }; + }); throw error; } - }; + } /** * @description updates the user tour completed status @@ -171,12 +188,12 @@ export class ProfileStore implements IUserProfileStore { updateTourCompleted = async () => { const isUserProfileTourCompleted = this.data.is_tour_completed || false; try { - runInAction(() => set(this.data, ["is_tour_completed"], true)); + this.mutateUserProfile({ is_tour_completed: true }); const userProfile = await this.userService.updateUserTourCompleted(); return userProfile; } catch (error) { runInAction(() => { - set(this.data, ["is_tour_completed"], isUserProfileTourCompleted); + this.mutateUserProfile({ is_tour_completed: isUserProfileTourCompleted }); this.error = { status: "user-profile-tour-complete-error", message: "Failed to update user profile is_tour_completed", diff --git a/web/core/store/workspace/index.ts b/web/core/store/workspace/index.ts index 4efba1e13..0cf5624e9 100644 --- a/web/core/store/workspace/index.ts +++ b/web/core/store/workspace/index.ts @@ -111,14 +111,19 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { */ fetchWorkspaces = async () => { this.loader = true; - const workspaceResponse = await this.workspaceService.userWorkspaces(); - runInAction(() => { - workspaceResponse.forEach((workspace) => { - set(this.workspaces, [workspace.id], workspace); + try { + const workspaceResponse = await this.workspaceService.userWorkspaces(); + runInAction(() => { + workspaceResponse.forEach((workspace) => { + set(this.workspaces, [workspace.id], workspace); + }); }); - }); - this.loader = false; - return workspaceResponse; + return workspaceResponse; + } catch (e) { + throw e; + } finally { + this.loader = false; + } }; /**