[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:
guru_sainath 2024-03-20 13:43:18 +05:30 committed by GitHub
parent 0f79c6d7d8
commit 7d3a96b3d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 128 additions and 34 deletions

View File

@ -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>

View File

@ -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 />

View File

@ -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">

View File

@ -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 &&

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -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";

View File

@ -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