forked from github/plane
[WEB-763] fix: workspace remains listed after leaving the workspace in the user profile (#3993)
* chore: build error * fix: workspace not getting removed when user leaves the workspace
This commit is contained in:
parent
0f79c6d7d8
commit
7d3a96b3d0
@ -178,7 +178,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="border-y border-custom-border-200">
|
||||
<div key={label.id} className="border-y border-custom-border-200">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 text-custom-text-100">
|
||||
<Component className="h-3 w-3" /> {label.name}
|
||||
</div>
|
||||
|
@ -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<IAppLayout> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandPalette />
|
||||
<UserAuthWrapper>
|
||||
<CommandPalette />
|
||||
<WorkspaceAuthWrapper>
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<AppSidebar />
|
||||
|
@ -34,15 +34,15 @@ export const UserAuthWrapper: FC<IUserAuthWrapper> = 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 (
|
||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
|
@ -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<IWorkspaceAuthWrapper> = 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 (
|
||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
@ -58,6 +93,58 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="relative w-full h-full flex flex-col justify-center items-center bg-custom-background-90">
|
||||
<div className="relative container px-5 md:px-0 w-full h-full mx-auto py-14 overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="flex-shrink-0 relative flex justify-between items-center gap-4">
|
||||
<div className="flex-shrink-0 py-4 z-10 bg-custom-background-90">
|
||||
<Image src={planeLogo} className="h-[26px] w-full" alt="Plane logo" />
|
||||
</div>
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="text-sm font-medium">{currentUser?.email}</div>
|
||||
<div
|
||||
className="relative flex-shrink-0 w-6 h-6 rounded overflow-hidden flex justify-center items-center cursor-pointer hover:bg-custom-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<Tooltip tooltipContent={"Sign out"} position="top" className="ml-2" isMobile={isMobile}>
|
||||
<LogOut size={14} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 w-full h-full flex-grow relative flex flex-col justify-center items-center">
|
||||
<div className="flex-shrink-0 relative">
|
||||
<Image src={WorkSpaceNotAvailable} className="h-[220px] object-contain object-center" alt="Plane logo" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-center">Workspace not found</h3>
|
||||
<p className="text-sm text-custom-text-200 text-center">
|
||||
No workspace found with the URL. It may not exist or you lack authorization to view it.
|
||||
</p>
|
||||
<div className="flex justify-center items-center gap-2 pt-4">
|
||||
{allWorkspaces && allWorkspaces.length > 1 && (
|
||||
<Link href="/">
|
||||
<Button>Go Home</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/profile">
|
||||
<Button variant="neutral-primary">Visit Profile</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-0 bottom-0 left-4 w-0 md:w-0.5 bg-custom-background-80" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// while user does not have access to view that workspace
|
||||
if (
|
||||
membership.hasPermissionToCurrentWorkspace !== undefined &&
|
||||
|
@ -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
|
||||
|
BIN
web/public/workspace/workspace-not-available.png
Normal file
BIN
web/public/workspace/workspace-not-available.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 188 KiB |
@ -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";
|
||||
|
@ -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<void>
|
||||
*/
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user