diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx index 3fe2022d4..32bcc4160 100644 --- a/web/components/issues/select/label.tsx +++ b/web/components/issues/select/label.tsx @@ -178,7 +178,7 @@ export const IssueLabelSelect: React.FC = observer((props) => { ); } else return ( -
+
{label.name}
diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index 6b376ee90..4ce3a9ab1 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -1,11 +1,11 @@ import { FC, ReactNode } from "react"; -// layouts import { observer } from "mobx-react-lite"; -import { CommandPalette } from "@/components/command-palette"; -import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "@/layouts/auth-layout"; // components -import { AppSidebar } from "./sidebar"; +import { CommandPalette } from "@/components/command-palette"; import { SidebarHamburgerToggle } from "@/components/core/sidebar/sidebar-menu-hamburger-toggle"; +// layouts +import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "@/layouts/auth-layout"; +import { AppSidebar } from "./sidebar"; export interface IAppLayout { children: ReactNode; @@ -19,8 +19,8 @@ export const AppLayout: FC = observer((props) => { return ( <> - +
diff --git a/web/layouts/auth-layout/user-wrapper.tsx b/web/layouts/auth-layout/user-wrapper.tsx index 36fd3cf00..e40f710bf 100644 --- a/web/layouts/auth-layout/user-wrapper.tsx +++ b/web/layouts/auth-layout/user-wrapper.tsx @@ -34,15 +34,15 @@ export const UserAuthWrapper: FC = observer((props) => { shouldRetryOnError: false, }); // fetching user settings - useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), { + const { isLoading: userSettingsLoader } = useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), { shouldRetryOnError: false, }); // fetching all workspaces - useSWR("USER_WORKSPACES_LIST", () => fetchWorkspaces(), { + const { isLoading: workspaceLoader } = useSWR("USER_WORKSPACES_LIST", () => fetchWorkspaces(), { shouldRetryOnError: false, }); - if (!currentUser && !currentUserError) { + if ((!currentUser && !currentUserError) || userSettingsLoader || workspaceLoader) { return (
diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index 467b0fb56..8b40597b2 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -1,12 +1,19 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import useSWR from "swr"; +import { useTheme } from "next-themes"; +import useSWR, { mutate } from "swr"; +import { LogOut } from "lucide-react"; // hooks -import { Button, Spinner } from "@plane/ui"; -import { useLabel, useMember, useProject, useUser } from "@/hooks/store"; -// icons +import { Button, Spinner, TOAST_TYPE, setToast, Tooltip } from "@plane/ui"; +import { useMember, useProject, useUser, useWorkspace } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +// images +import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; +import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; +import WorkSpaceNotAvailable from "public/workspace/workspace-not-available.png"; export interface IWorkspaceAuthWrapper { children: ReactNode; @@ -14,42 +21,70 @@ export interface IWorkspaceAuthWrapper { export const WorkspaceAuthWrapper: FC = observer((props) => { const { children } = props; + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + // next themes + const { resolvedTheme, setTheme } = useTheme(); // store hooks - const { membership } = useUser(); + const { membership, signOut, currentUser } = useUser(); const { fetchProjects } = useProject(); const { workspace: { fetchWorkspaceMembers }, } = useMember(); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaces } = useWorkspace(); + const { isMobile } = usePlatformOS(); + + const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo; + const allWorkspaces = workspaces ? Object.values(workspaces) : undefined; + const currentWorkspace = + (allWorkspaces && allWorkspaces.find((workspace) => workspace?.slug === workspaceSlug)) || undefined; + // fetching user workspace information useSWR( - workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, - workspaceSlug ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null, + workspaceSlug && currentWorkspace ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, + workspaceSlug && currentWorkspace ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching workspace projects useSWR( - workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null, - workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null, + workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null, + workspaceSlug && currentWorkspace ? () => fetchProjects(workspaceSlug.toString()) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace members useSWR( - workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null, + workspaceSlug && currentWorkspace ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, + workspaceSlug && currentWorkspace ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace user projects role useSWR( - workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null, - workspaceSlug ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) : null, + workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null, + workspaceSlug && currentWorkspace + ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) + : null, { revalidateIfStale: false, revalidateOnFocus: false } ); - // while data is being loaded - if (!membership.currentWorkspaceMemberInfo && membership.hasPermissionToCurrentWorkspace === undefined) { + const handleSignOut = async () => { + await signOut() + .then(() => { + mutate("CURRENT_USER_DETAILS", null); + setTheme("system"); + router.push("/"); + }) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Failed to sign out. Please try again.", + }) + ); + }; + + // if list of workspaces are not there then we have to render the spinner + if (allWorkspaces === undefined) { return (
@@ -58,6 +93,58 @@ export const WorkspaceAuthWrapper: FC = observer((props)
); } + + // if workspaces are there and we are trying to access the workspace that we are not part of then show the existing workspaces + if ( + currentWorkspace === undefined && + !membership.currentWorkspaceMemberInfo && + membership.hasPermissionToCurrentWorkspace === undefined + ) { + return ( +
+
+
+
+ Plane logo +
+
+
{currentUser?.email}
+
+ + + +
+
+
+
+
+ Plane logo +
+

Workspace not found

+

+ No workspace found with the URL. It may not exist or you lack authorization to view it. +

+
+ {allWorkspaces && allWorkspaces.length > 1 && ( + + + + )} + + + +
+
+ +
+
+
+ ); + } + // while user does not have access to view that workspace if ( membership.hasPermissionToCurrentWorkspace !== undefined && diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index b40ff93bd..b3efbe2c4 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -43,6 +43,7 @@ export const ProfileLayoutSidebar = observer(() => { const { currentUser, currentUserSettings, signOut } = useUser(); const { workspaces } = useWorkspace(); const { isMobile } = usePlatformOS(); + const workspacesList = Object.values(workspaces ?? {}); // redirect url for normal mode diff --git a/web/public/workspace/workspace-not-available.png b/web/public/workspace/workspace-not-available.png new file mode 100644 index 000000000..e95cdf02a Binary files /dev/null and b/web/public/workspace/workspace-not-available.png differ diff --git a/web/store/user/index.ts b/web/store/user/index.ts index c7e07e1fc..1a7a8778f 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -1,9 +1,9 @@ import { action, observable, runInAction, makeObservable } from "mobx"; +// interfaces +import { IUser, IUserSettings } from "@plane/types"; // services import { AuthService } from "@/services/auth.service"; import { UserService } from "@/services/user.service"; -// interfaces -import { IUser, IUserSettings } from "@plane/types"; // store import { RootStore } from "../root.store"; import { IUserMembershipStore, UserMembershipStore } from "./user-membership.store"; diff --git a/web/store/user/user-membership.store.ts b/web/store/user/user-membership.store.ts index 400de4960..407157d20 100644 --- a/web/store/user/user-membership.store.ts +++ b/web/store/user/user-membership.store.ts @@ -1,15 +1,16 @@ import { set } from "lodash"; import { action, observable, runInAction, makeObservable, computed } from "mobx"; -// services +// types +import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/types"; +// constants import { EUserProjectRoles } from "@/constants/project"; import { EUserWorkspaceRoles } from "@/constants/workspace"; +// services import { ProjectMemberService } from "@/services/project"; import { UserService } from "@/services/user.service"; import { WorkspaceService } from "@/services/workspace.service"; -// interfaces -import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/types"; +// store import { RootStore } from "../root.store"; -// constants export interface IUserMembershipStore { // observables @@ -61,6 +62,7 @@ export class UserMembershipStore implements IUserMembershipStore { workspaceProjectsRole: { [workspaceSlug: string]: IUserProjectsRole } = {}; // stores router; + store; // services userService; workspaceService; @@ -91,6 +93,7 @@ export class UserMembershipStore implements IUserMembershipStore { fetchUserWorkspaceProjectsRole: action, }); this.router = _rootStore.app.router; + this.store = _rootStore; // services this.userService = new UserService(); this.workspaceService = new WorkspaceService(); @@ -193,13 +196,16 @@ export class UserMembershipStore implements IUserMembershipStore { * @param workspaceSlug * @returns Promise */ - leaveWorkspace = async (workspaceSlug: string) => + leaveWorkspace = async (workspaceSlug: string) => { + const currentWorksSpace = this.store.workspaceRoot?.currentWorkspace; await this.userService.leaveWorkspace(workspaceSlug).then(() => { runInAction(() => { + if (currentWorksSpace) delete this.store.workspaceRoot?.workspaces?.[currentWorksSpace?.id]; delete this.workspaceMemberInfo[workspaceSlug]; delete this.hasPermissionToWorkspace[workspaceSlug]; }); }); + }; /** * Joins a project