base
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { GANTT_TIMELINE_TYPE } from "@plane/types";
|
||||
// components
|
||||
import { ProjectAccessRestriction } from "@/components/auth-screens/project/project-access-restriction";
|
||||
import {
|
||||
PROJECT_DETAILS,
|
||||
PROJECT_ME_INFORMATION,
|
||||
PROJECT_LABELS,
|
||||
PROJECT_MEMBERS,
|
||||
PROJECT_MEMBER_PREFERENCES,
|
||||
PROJECT_STATES,
|
||||
PROJECT_ESTIMATES,
|
||||
PROJECT_ALL_CYCLES,
|
||||
PROJECT_MODULES,
|
||||
PROJECT_VIEWS,
|
||||
PROJECT_INTAKE_STATE,
|
||||
} from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useProjectEstimates } from "@/hooks/store/estimates";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
|
||||
|
||||
interface IProjectAuthWrapper {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
children: ReactNode;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IProjectAuthWrapper) {
|
||||
const { workspaceSlug, projectId, children, isLoading: isParentLoading = false } = props;
|
||||
// states
|
||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||
// store hooks
|
||||
const { fetchUserProjectInfo, allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||
const { fetchProjectDetails } = useProject();
|
||||
const { joinProject } = useUserPermissions();
|
||||
const { fetchAllCycles } = useCycle();
|
||||
const { fetchModulesSlim, fetchModules } = useModule();
|
||||
const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE);
|
||||
const { fetchViews } = useProjectView();
|
||||
const {
|
||||
project: { fetchProjectMembers, fetchProjectUserProperties },
|
||||
} = useMember();
|
||||
const { fetchProjectStates, fetchProjectIntakeState } = useProjectState();
|
||||
const { data: currentUserData } = useUser();
|
||||
const { fetchProjectLabels } = useLabel();
|
||||
const { getProjectEstimates } = useProjectEstimates();
|
||||
// derived values
|
||||
const hasPermissionToCurrentProject = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE, workspaceSlug);
|
||||
// Initialize module timeline chart
|
||||
useEffect(() => {
|
||||
initGantt();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// fetching project details
|
||||
const { isLoading: isProjectDetailsLoading, error: projectDetailsError } = useSWR(
|
||||
PROJECT_DETAILS(workspaceSlug, projectId),
|
||||
() => fetchProjectDetails(workspaceSlug, projectId)
|
||||
);
|
||||
// fetching user project member information
|
||||
useSWR(PROJECT_ME_INFORMATION(workspaceSlug, projectId), () => fetchUserProjectInfo(workspaceSlug, projectId));
|
||||
// fetching project member preferences
|
||||
useSWR(
|
||||
currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(projectId, currentProjectRole) : null,
|
||||
currentUserData?.id ? () => fetchProjectUserProperties(workspaceSlug, projectId) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project labels
|
||||
useSWR(PROJECT_LABELS(projectId, currentProjectRole), () => fetchProjectLabels(workspaceSlug, projectId), {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
// fetching project members
|
||||
useSWR(PROJECT_MEMBERS(projectId, currentProjectRole), () => fetchProjectMembers(workspaceSlug, projectId), {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
// fetching project states
|
||||
useSWR(PROJECT_STATES(projectId, currentProjectRole), () => fetchProjectStates(workspaceSlug, projectId), {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
// fetching project intake state
|
||||
useSWR(PROJECT_INTAKE_STATE(projectId, currentProjectRole), () => fetchProjectIntakeState(workspaceSlug, projectId), {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
// fetching project estimates
|
||||
useSWR(PROJECT_ESTIMATES(projectId, currentProjectRole), () => getProjectEstimates(workspaceSlug, projectId), {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
// fetching project cycles
|
||||
useSWR(PROJECT_ALL_CYCLES(projectId, currentProjectRole), () => fetchAllCycles(workspaceSlug, projectId), {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
// fetching project modules
|
||||
useSWR(
|
||||
PROJECT_MODULES(projectId, currentProjectRole),
|
||||
async () => {
|
||||
await Promise.all([fetchModulesSlim(workspaceSlug, projectId), fetchModules(workspaceSlug, projectId)]);
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project views
|
||||
useSWR(PROJECT_VIEWS(projectId, currentProjectRole), () => fetchViews(workspaceSlug, projectId), {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
// handle join project
|
||||
const handleJoinProject = () => {
|
||||
setIsJoiningProject(true);
|
||||
joinProject(workspaceSlug, projectId).finally(() => setIsJoiningProject(false));
|
||||
};
|
||||
|
||||
const isProjectLoading = (isParentLoading || isProjectDetailsLoading) && !projectDetailsError;
|
||||
|
||||
if (isProjectLoading) return null;
|
||||
|
||||
if (!isProjectLoading && hasPermissionToCurrentProject === false) {
|
||||
return (
|
||||
<ProjectAccessRestriction
|
||||
errorStatusCode={projectDetailsError?.status}
|
||||
isWorkspaceAdmin={isWorkspaceAdmin}
|
||||
handleJoinProject={handleJoinProject}
|
||||
isJoinButtonDisabled={isJoiningProject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
import { LogOut } from "lucide-react";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { PlaneLogo } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// assets
|
||||
import WorkSpaceNotAvailable from "@/app/assets/workspace/workspace-not-available.png?url";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// constants
|
||||
import {
|
||||
WORKSPACE_MEMBERS,
|
||||
WORKSPACE_PARTIAL_PROJECTS,
|
||||
WORKSPACE_MEMBER_ME_INFORMATION,
|
||||
WORKSPACE_PROJECTS_ROLES_INFORMATION,
|
||||
WORKSPACE_FAVORITE,
|
||||
WORKSPACE_STATES,
|
||||
WORKSPACE_SIDEBAR_PREFERENCES,
|
||||
WORKSPACE_PROJECT_NAVIGATION_PREFERENCES,
|
||||
} from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
interface IWorkspaceAuthWrapper {
|
||||
children: ReactNode;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceAuthWrapper = observer(function WorkspaceAuthWrapper(props: IWorkspaceAuthWrapper) {
|
||||
const { children, isLoading: isParentLoading = false } = props;
|
||||
const { t } = useTranslation();
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { signOut, data: currentUser } = useUser();
|
||||
const { fetchPartialProjects } = useProject();
|
||||
const { fetchFavorite } = useFavorite();
|
||||
const {
|
||||
workspace: { fetchWorkspaceMembers },
|
||||
} = useMember();
|
||||
const { workspaces, fetchSidebarNavigationPreferences, fetchProjectNavigationPreferences } = useWorkspace();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { loader, workspaceInfoBySlug, fetchUserWorkspaceInfo, fetchUserProjectPermissions, allowPermissions } =
|
||||
useUserPermissions();
|
||||
const { fetchWorkspaceStates } = useProjectState();
|
||||
// derived values
|
||||
const canPerformWorkspaceMemberActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
const allWorkspaces = workspaces ? Object.values(workspaces) : undefined;
|
||||
const currentWorkspace =
|
||||
(allWorkspaces && allWorkspaces.find((workspace) => workspace?.slug === workspaceSlug)) || undefined;
|
||||
const currentWorkspaceInfo = workspaceSlug && workspaceInfoBySlug(workspaceSlug.toString());
|
||||
|
||||
// fetching user workspace information
|
||||
useSWR(
|
||||
workspaceSlug && currentWorkspace ? WORKSPACE_MEMBER_ME_INFORMATION(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && currentWorkspace ? () => fetchUserWorkspaceInfo(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
useSWR(
|
||||
workspaceSlug && currentWorkspace ? WORKSPACE_PROJECTS_ROLES_INFORMATION(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && currentWorkspace ? () => fetchUserProjectPermissions(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetching workspace projects
|
||||
useSWR(
|
||||
workspaceSlug && currentWorkspace ? WORKSPACE_PARTIAL_PROJECTS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && currentWorkspace ? () => fetchPartialProjects(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetch workspace members
|
||||
useSWR(
|
||||
workspaceSlug && currentWorkspace ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && currentWorkspace ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetch workspace favorite
|
||||
useSWR(
|
||||
workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions
|
||||
? WORKSPACE_FAVORITE(workspaceSlug.toString())
|
||||
: null,
|
||||
workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions
|
||||
? () => fetchFavorite(workspaceSlug.toString())
|
||||
: null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetch workspace states
|
||||
useSWR(
|
||||
workspaceSlug ? WORKSPACE_STATES(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch workspace sidebar preferences
|
||||
useSWR(
|
||||
workspaceSlug ? WORKSPACE_SIDEBAR_PREFERENCES(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => fetchSidebarNavigationPreferences(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch workspace project navigation preferences
|
||||
useSWR(
|
||||
workspaceSlug ? WORKSPACE_PROJECT_NAVIGATION_PREFERENCES(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => fetchProjectNavigationPreferences(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut().catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("auth.sign_out.toast.error.title"),
|
||||
message: t("auth.sign_out.toast.error.message"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// if list of workspaces are not there then we have to render the spinner
|
||||
if (isParentLoading || allWorkspaces === undefined || loader) {
|
||||
return (
|
||||
<div className="grid h-full place-items-center rounded-lg border border-subtle p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<LogoSpinner />
|
||||
</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 && !currentWorkspaceInfo) {
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-center bg-surface-2">
|
||||
<div className="relative container mx-auto flex h-full w-full flex-col overflow-hidden overflow-y-auto px-5 py-14 md:px-0">
|
||||
<div className="relative flex flex-shrink-0 items-center justify-between gap-4">
|
||||
<div className="z-10 flex-shrink-0 bg-surface-2 py-4">
|
||||
<PlaneLogo className="h-9 w-auto text-primary" />
|
||||
</div>
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="text-13 font-medium">{currentUser?.email}</div>
|
||||
<div
|
||||
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm hover:bg-layer-1"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<Tooltip tooltipContent={t("sign_out")} position="top" className="ml-2" isMobile={isMobile}>
|
||||
<LogOut size={14} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex h-full w-full flex-grow flex-col items-center justify-center space-y-3">
|
||||
<div className="relative flex-shrink-0">
|
||||
<img src={WorkSpaceNotAvailable} className="h-[220px] object-contain object-center" alt="NODE.DC logo" />
|
||||
</div>
|
||||
<h3 className="text-center text-16 font-semibold">{t("workspace_access.workspace_not_found_title")}</h3>
|
||||
<p className="text-center text-13 text-secondary">
|
||||
{t("workspace_access.workspace_not_found_description")}
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2 pt-4">
|
||||
{allWorkspaces && allWorkspaces.length > 0 && (
|
||||
<Link href="/" className={cn(getButtonStyling("primary", "base"))}>
|
||||
{t("go_home")}
|
||||
</Link>
|
||||
)}
|
||||
{allWorkspaces?.length > 0 && (
|
||||
<Link href="/settings/profile/general/" className={cn(getButtonStyling("secondary", "base"))}>
|
||||
{t("workspace_access.profile_button")}
|
||||
</Link>
|
||||
)}
|
||||
{allWorkspaces && allWorkspaces.length === 0 && (
|
||||
<Link href="/create-workspace/" className={cn(getButtonStyling("secondary", "base"))}>
|
||||
{t("create_workspace")}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-0 bottom-0 left-4 w-0 bg-layer-1 md:w-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// while user does not have access to view that workspace
|
||||
if (currentWorkspaceInfo === undefined) {
|
||||
return (
|
||||
<div className={`h-screen w-full overflow-hidden bg-surface-1`}>
|
||||
<div className="grid h-full place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-16 font-semibold">{t("workspace_access.not_authorized_title")}</h3>
|
||||
<p className="mx-auto w-1/2 text-13 text-secondary">
|
||||
{t("workspace_access.not_authorized_description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Link href="/invitations">
|
||||
<span>
|
||||
<Button variant="secondary">{t("workspace_access.pending_invites_button")}</Button>
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/create-workspace">
|
||||
<span>
|
||||
<Button variant="primary">{t("create_workspace")}</Button>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
gradient?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function DefaultLayout({ children, gradient = false, className }: Props) {
|
||||
return (
|
||||
<div className={cn(`h-screen w-full overflow-hidden ${gradient ? "" : "bg-surface-1"}`, className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DefaultLayout;
|
||||
Reference in New Issue
Block a user