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
|
} else
|
||||||
return (
|
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">
|
<div className="flex select-none items-center gap-2 truncate p-2 text-custom-text-100">
|
||||||
<Component className="h-3 w-3" /> {label.name}
|
<Component className="h-3 w-3" /> {label.name}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
// layouts
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { CommandPalette } from "@/components/command-palette";
|
|
||||||
import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "@/layouts/auth-layout";
|
|
||||||
// components
|
// components
|
||||||
import { AppSidebar } from "./sidebar";
|
import { CommandPalette } from "@/components/command-palette";
|
||||||
import { SidebarHamburgerToggle } from "@/components/core/sidebar/sidebar-menu-hamburger-toggle";
|
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 {
|
export interface IAppLayout {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -19,8 +19,8 @@ export const AppLayout: FC<IAppLayout> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CommandPalette />
|
|
||||||
<UserAuthWrapper>
|
<UserAuthWrapper>
|
||||||
|
<CommandPalette />
|
||||||
<WorkspaceAuthWrapper>
|
<WorkspaceAuthWrapper>
|
||||||
<div className="relative flex h-screen w-full overflow-hidden">
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
@ -34,15 +34,15 @@ export const UserAuthWrapper: FC<IUserAuthWrapper> = observer((props) => {
|
|||||||
shouldRetryOnError: false,
|
shouldRetryOnError: false,
|
||||||
});
|
});
|
||||||
// fetching user settings
|
// fetching user settings
|
||||||
useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), {
|
const { isLoading: userSettingsLoader } = useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), {
|
||||||
shouldRetryOnError: false,
|
shouldRetryOnError: false,
|
||||||
});
|
});
|
||||||
// fetching all workspaces
|
// fetching all workspaces
|
||||||
useSWR("USER_WORKSPACES_LIST", () => fetchWorkspaces(), {
|
const { isLoading: workspaceLoader } = useSWR("USER_WORKSPACES_LIST", () => fetchWorkspaces(), {
|
||||||
shouldRetryOnError: false,
|
shouldRetryOnError: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!currentUser && !currentUserError) {
|
if ((!currentUser && !currentUserError) || userSettingsLoader || workspaceLoader) {
|
||||||
return (
|
return (
|
||||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
<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">
|
<div className="flex flex-col items-center gap-3 text-center">
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
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
|
// hooks
|
||||||
import { Button, Spinner } from "@plane/ui";
|
import { Button, Spinner, TOAST_TYPE, setToast, Tooltip } from "@plane/ui";
|
||||||
import { useLabel, useMember, useProject, useUser } from "@/hooks/store";
|
import { useMember, useProject, useUser, useWorkspace } from "@/hooks/store";
|
||||||
// icons
|
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 {
|
export interface IWorkspaceAuthWrapper {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -14,42 +21,70 @@ export interface IWorkspaceAuthWrapper {
|
|||||||
|
|
||||||
export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props) => {
|
export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
// next themes
|
||||||
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { membership } = useUser();
|
const { membership, signOut, currentUser } = useUser();
|
||||||
const { fetchProjects } = useProject();
|
const { fetchProjects } = useProject();
|
||||||
const {
|
const {
|
||||||
workspace: { fetchWorkspaceMembers },
|
workspace: { fetchWorkspaceMembers },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
// router
|
const { workspaces } = useWorkspace();
|
||||||
const router = useRouter();
|
const { isMobile } = usePlatformOS();
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
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
|
// fetching user workspace information
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null,
|
workspaceSlug && currentWorkspace ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null,
|
workspaceSlug && currentWorkspace ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetching workspace projects
|
// fetching workspace projects
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null,
|
workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null,
|
workspaceSlug && currentWorkspace ? () => fetchProjects(workspaceSlug.toString()) : null,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetch workspace members
|
// fetch workspace members
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
workspaceSlug && currentWorkspace ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null,
|
workspaceSlug && currentWorkspace ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetch workspace user projects role
|
// fetch workspace user projects role
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
|
workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) : null,
|
workspaceSlug && currentWorkspace
|
||||||
|
? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString())
|
||||||
|
: null,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
// while data is being loaded
|
const handleSignOut = async () => {
|
||||||
if (!membership.currentWorkspaceMemberInfo && membership.hasPermissionToCurrentWorkspace === undefined) {
|
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 (
|
return (
|
||||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
<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">
|
<div className="flex flex-col items-center gap-3 text-center">
|
||||||
@ -58,6 +93,58 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||||||
</div>
|
</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
|
// while user does not have access to view that workspace
|
||||||
if (
|
if (
|
||||||
membership.hasPermissionToCurrentWorkspace !== undefined &&
|
membership.hasPermissionToCurrentWorkspace !== undefined &&
|
||||||
|
@ -43,6 +43,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
const { currentUser, currentUserSettings, signOut } = useUser();
|
const { currentUser, currentUserSettings, signOut } = useUser();
|
||||||
const { workspaces } = useWorkspace();
|
const { workspaces } = useWorkspace();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
const workspacesList = Object.values(workspaces ?? {});
|
const workspacesList = Object.values(workspaces ?? {});
|
||||||
|
|
||||||
// redirect url for normal mode
|
// 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";
|
import { action, observable, runInAction, makeObservable } from "mobx";
|
||||||
|
// interfaces
|
||||||
|
import { IUser, IUserSettings } from "@plane/types";
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
import { UserService } from "@/services/user.service";
|
import { UserService } from "@/services/user.service";
|
||||||
// interfaces
|
|
||||||
import { IUser, IUserSettings } from "@plane/types";
|
|
||||||
// store
|
// store
|
||||||
import { RootStore } from "../root.store";
|
import { RootStore } from "../root.store";
|
||||||
import { IUserMembershipStore, UserMembershipStore } from "./user-membership.store";
|
import { IUserMembershipStore, UserMembershipStore } from "./user-membership.store";
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { set } from "lodash";
|
import { set } from "lodash";
|
||||||
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
||||||
// services
|
// types
|
||||||
|
import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/types";
|
||||||
|
// constants
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
|
// services
|
||||||
import { ProjectMemberService } from "@/services/project";
|
import { ProjectMemberService } from "@/services/project";
|
||||||
import { UserService } from "@/services/user.service";
|
import { UserService } from "@/services/user.service";
|
||||||
import { WorkspaceService } from "@/services/workspace.service";
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
// interfaces
|
// store
|
||||||
import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/types";
|
|
||||||
import { RootStore } from "../root.store";
|
import { RootStore } from "../root.store";
|
||||||
// constants
|
|
||||||
|
|
||||||
export interface IUserMembershipStore {
|
export interface IUserMembershipStore {
|
||||||
// observables
|
// observables
|
||||||
@ -61,6 +62,7 @@ export class UserMembershipStore implements IUserMembershipStore {
|
|||||||
workspaceProjectsRole: { [workspaceSlug: string]: IUserProjectsRole } = {};
|
workspaceProjectsRole: { [workspaceSlug: string]: IUserProjectsRole } = {};
|
||||||
// stores
|
// stores
|
||||||
router;
|
router;
|
||||||
|
store;
|
||||||
// services
|
// services
|
||||||
userService;
|
userService;
|
||||||
workspaceService;
|
workspaceService;
|
||||||
@ -91,6 +93,7 @@ export class UserMembershipStore implements IUserMembershipStore {
|
|||||||
fetchUserWorkspaceProjectsRole: action,
|
fetchUserWorkspaceProjectsRole: action,
|
||||||
});
|
});
|
||||||
this.router = _rootStore.app.router;
|
this.router = _rootStore.app.router;
|
||||||
|
this.store = _rootStore;
|
||||||
// services
|
// services
|
||||||
this.userService = new UserService();
|
this.userService = new UserService();
|
||||||
this.workspaceService = new WorkspaceService();
|
this.workspaceService = new WorkspaceService();
|
||||||
@ -193,13 +196,16 @@ export class UserMembershipStore implements IUserMembershipStore {
|
|||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
leaveWorkspace = async (workspaceSlug: string) =>
|
leaveWorkspace = async (workspaceSlug: string) => {
|
||||||
|
const currentWorksSpace = this.store.workspaceRoot?.currentWorkspace;
|
||||||
await this.userService.leaveWorkspace(workspaceSlug).then(() => {
|
await this.userService.leaveWorkspace(workspaceSlug).then(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
if (currentWorksSpace) delete this.store.workspaceRoot?.workspaces?.[currentWorksSpace?.id];
|
||||||
delete this.workspaceMemberInfo[workspaceSlug];
|
delete this.workspaceMemberInfo[workspaceSlug];
|
||||||
delete this.hasPermissionToWorkspace[workspaceSlug];
|
delete this.hasPermissionToWorkspace[workspaceSlug];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Joins a project
|
* Joins a project
|
||||||
|
Loading…
Reference in New Issue
Block a user