This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -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;