[WEB-4338] fix: incorrect error code in project retrieve API (#7234)

* fix: project error message and status code

* fix: incorrect member role check

* fix: project error message and status code

* fix: improve project permission checks and error handling in ProjectViewSet

* feat: enhance project settings layout with better loading strategy and fix all flicker

* fix: prevent rendering during project loading in ProjectAuthWrapper

* refactor: adjust layout structure in ProjectDetailSettingsLayout and enhance access restriction logic in ProjectAccessRestriction

* refactor: replace ProjectAccessRestriction component with updated version and enhance error handling

- Deleted the old ProjectAccessRestriction component.
- Introduced a new ProjectAccessRestriction component with improved error handling and user prompts for joining projects.
- Updated translations for new error states in multiple languages.

* fix: enhance error handling in IssueDetailsPage and remove JoinProject component

---------

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
Sangeetha 2025-12-01 17:14:01 +05:30 committed by GitHub
parent 8db95d9ec0
commit 60220801ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 609 additions and 516 deletions

View file

@ -1,43 +1,43 @@
# Python imports
import boto3
from django.conf import settings
from django.utils import timezone
import json
import boto3
# Django imports
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.utils import timezone
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
# Module imports
from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.app.permissions import ROLE, ProjectMemberPermission, allow_permission
from plane.app.serializers import (
ProjectSerializer,
ProjectListSerializer,
DeployBoardSerializer,
ProjectListSerializer,
ProjectSerializer,
)
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.db.models import (
UserFavorite,
Intake,
DeployBoard,
Intake,
IssueUserProperty,
Project,
ProjectIdentifier,
ProjectMember,
ProjectNetwork,
State,
DEFAULT_STATES,
Workspace,
WorkspaceMember,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.exception_logger import log_exception
from plane.utils.host import base_host
@ -210,19 +210,25 @@ class ProjectViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def retrieve(self, request, slug, pk):
project = (
self.get_queryset()
.filter(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
.filter(archived_at__isnull=True)
.filter(pk=pk)
).first()
project = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk).first()
if project is None:
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
member_ids = [str(project_member.member_id) for project_member in project.members_list]
if str(request.user.id) not in member_ids:
if project.network == ProjectNetwork.SECRET.value:
return Response(
{"error": "You do not have permission"},
status=status.HTTP_403_FORBIDDEN,
)
else:
return Response(
{"error": "You are not a member of this project"},
status=status.HTTP_409_CONFLICT,
)
recent_visited_task.delay(
slug=slug,
project_id=pk,

View file

@ -52,6 +52,7 @@ from .project import (
ProjectIdentifier,
ProjectMember,
ProjectMemberInvite,
ProjectNetwork,
ProjectPublicMember,
)
from .session import Session

View file

@ -82,7 +82,7 @@ function IssueDetailsPage({ params }: Route.ComponentProps) {
return (
<>
<PageHead title={pageTitle} />
{error ? (
{error && !issueLoader ? (
<EmptyState
image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight}
title={t("issue.empty_state.issue_detail.title")}

View file

@ -11,6 +11,7 @@ import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-butt
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences";
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
// local imports
import type { Route } from "./+types/layout";
@ -44,7 +45,9 @@ function ProjectLayout({ params }: Route.ComponentProps) {
</Row>
</div>
)}
<Outlet />
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
<Outlet />
</ProjectAuthWrapper>
</>
);
}

View file

@ -1,14 +0,0 @@
import { Outlet } from "react-router";
// plane web layouts
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
import type { Route } from "./+types/layout";
export default function ProjectDetailLayout({ params }: Route.ComponentProps) {
// router
const { workspaceSlug, projectId } = params;
return (
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
<Outlet />
</ProjectAuthWrapper>
);
}

View file

@ -1,7 +1,6 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PageHead } from "@/components/core/page-title";
import { ProfileForm } from "@/components/profile/form";
// hooks
@ -12,13 +11,7 @@ function ProfileSettingsPage() {
// store hooks
const { data: currentUser, userProfile } = useUser();
if (!currentUser)
return (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<LogoSpinner />
</div>
);
if (!currentUser) return <></>;
return (
<>
<PageHead title={`${t("profile.label")} - ${t("general_settings")}`} />

View file

@ -2,7 +2,6 @@ import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PageHead } from "@/components/core/page-title";
import { PreferencesList } from "@/components/preferences/list";
import { LanguageTimezone } from "@/components/profile/preferences/language-timezone";
@ -16,30 +15,23 @@ function ProfileAppearancePage() {
// hooks
const { data: userProfile } = useUserProfile();
if (!userProfile) return <></>;
return (
<>
<PageHead title={`${t("profile.label")} - ${t("preferences")}`} />
{userProfile ? (
<>
<div className="flex flex-col gap-4 w-full">
<div>
<SettingsHeading
title={t("account_settings.preferences.heading")}
description={t("account_settings.preferences.description")}
/>
<PreferencesList />
</div>
<div>
<ProfileSettingContentHeader title={t("language_and_time")} />
<LanguageTimezone />
</div>
</div>
</>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<LogoSpinner />
<div className="flex flex-col gap-4 w-full">
<div>
<SettingsHeading
title={t("account_settings.preferences.heading")}
description={t("account_settings.preferences.description")}
/>
<PreferencesList />
</div>
)}
<div>
<ProfileSettingContentHeader title={t("language_and_time")} />
<LanguageTimezone />
</div>
</div>
</>
);
}

View file

@ -0,0 +1,33 @@
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Outlet } from "react-router";
// components
import { getProjectActivePath } from "@/components/settings/helper";
import { SettingsMobileNav } from "@/components/settings/mobile";
import { ProjectSettingsSidebar } from "@/components/settings/project/sidebar";
// plane web imports
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
// types
import type { Route } from "./+types/layout";
function ProjectDetailSettingsLayout({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// router
const pathname = usePathname();
return (
<>
<SettingsMobileNav hamburgerContent={ProjectSettingsSidebar} activePath={getProjectActivePath(pathname) || ""} />
<div className="relative flex h-full w-full">
<div className="hidden md:block">{projectId && <ProjectSettingsSidebar />}</div>
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
<div className="w-full h-full overflow-y-scroll md:pt-page-y">
<Outlet />
</div>
</ProjectAuthWrapper>
</div>
</>
);
}
export default observer(ProjectDetailSettingsLayout);

View file

@ -1,6 +1,5 @@
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
// components
@ -24,12 +23,8 @@ function ProjectSettingsPage({ params }: Route.ComponentProps) {
// router
const { workspaceSlug, projectId } = params;
// store hooks
const { currentProjectDetails, fetchProjectDetails } = useProject();
const { currentProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// api call to fetch project details
// TODO: removed this API if not necessary
const { isLoading } = useSWR(`PROJECT_DETAILS_${projectId}`, () => fetchProjectDetails(workspaceSlug, projectId));
// derived values
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId);
@ -56,7 +51,7 @@ function ProjectSettingsPage({ params }: Route.ComponentProps) {
)}
<div className={`w-full ${isAdmin ? "" : "opacity-60"}`}>
{currentProjectDetails && !isLoading ? (
{currentProjectDetails ? (
<ProjectDetailsForm
project={currentProjectDetails}
workspaceSlug={workspaceSlug}

View file

@ -1,21 +1,17 @@
import { useEffect } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Outlet } from "react-router";
// components
import { getProjectActivePath } from "@/components/settings/helper";
import { SettingsMobileNav } from "@/components/settings/mobile";
import { ProjectSettingsSidebar } from "@/components/settings/project/sidebar";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router";
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
// types
import type { Route } from "./+types/layout";
function ProjectSettingsLayout({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// router
const router = useAppRouter();
const pathname = usePathname();
const { workspaceSlug, projectId } = params;
// store hooks
const { joinedProjectIds } = useProject();
useEffect(() => {
@ -25,19 +21,7 @@ function ProjectSettingsLayout({ params }: Route.ComponentProps) {
}
}, [joinedProjectIds, router, workspaceSlug, projectId]);
return (
<>
<SettingsMobileNav hamburgerContent={ProjectSettingsSidebar} activePath={getProjectActivePath(pathname) || ""} />
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
<div className="relative flex h-full w-full">
<div className="hidden md:block">{projectId && <ProjectSettingsSidebar />}</div>
<div className="w-full h-full overflow-y-scroll md:pt-page-y">
<Outlet />
</div>
</div>
</ProjectAuthWrapper>
</>
);
return <Outlet />;
}
export default observer(ProjectSettingsLayout);

View file

@ -1,8 +1,14 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { useTheme } from "next-themes";
// plane imports
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { cn } from "@plane/utils";
// assets
import ProjectDarkEmptyState from "@/app/assets/empty-state/project-settings/no-projects-dark.png?url";
import ProjectLightEmptyState from "@/app/assets/empty-state/project-settings/no-projects-light.png?url";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
function ProjectSettingsPage() {
@ -10,13 +16,10 @@ function ProjectSettingsPage() {
const { resolvedTheme } = useTheme();
const { toggleCreateProjectModal } = useCommandPalette();
// derived values
const resolvedPath =
resolvedTheme === "dark"
? "/empty-state/project-settings/no-projects-dark.png"
: "/empty-state/project-settings/no-projects-light.png";
const resolvedPath = resolvedTheme === "dark" ? ProjectDarkEmptyState : ProjectLightEmptyState;
return (
<div className="flex flex-col gap-4 items-center justify-center h-full max-w-[480px] mx-auto">
<img src={resolvedPath} className="w-full h-full object-contain" alt="No projects yet" />
<img src={resolvedPath} alt="No projects yet" />
<div className="text-lg font-semibold text-custom-text-350">No projects yet</div>
<div className="text-sm text-custom-text-350 text-center">
Projects act as the foundation for goal-driven work. They let you manage your teams, tasks, and everything you
@ -38,4 +41,4 @@ function ProjectSettingsPage() {
);
}
export default ProjectSettingsPage;
export default observer(ProjectSettingsPage);

View file

@ -108,9 +108,13 @@ export const coreRoutes: RouteConfigEntry[] = [
),
]),
// ====================================================================
// PROJECT LEVEL ROUTES
// ====================================================================
// Archived Projects
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx", [
route(
":workspaceSlug/projects/archives",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx"
),
]),
// --------------------------------------------------------------------
// PROJECT LEVEL ROUTES
@ -122,136 +126,123 @@ export const coreRoutes: RouteConfigEntry[] = [
]),
// Project Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx", [
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [
// Project Issues List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/issues",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx"
),
]),
// Issue Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [
// Project Issues List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/issues/:issueId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx"
),
// Cycle Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/cycles/:cycleId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx"
),
]),
// Cycles List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/cycles",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx"
),
]),
// Module Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/modules/:moduleId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx"
),
]),
// Modules List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/modules",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx"
),
]),
// View Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/views/:viewId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx"
),
]),
// Views List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/views",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx"
),
]),
// Page Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/pages/:pageId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx"
),
]),
// Pages List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/pages",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx"
),
]),
// Intake list
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/intake",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx"
),
]),
]),
// Archived Projects
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx", [
route(
":workspaceSlug/projects/archives",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx"
":workspaceSlug/projects/:projectId/issues",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx"
),
]),
// Project Archives - Issues, Cycles, Modules
// Project Archives - Issues - List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/archives/issues",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx"
),
]),
// Project Archives - Issues - Detail
layout(
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx",
[
route(
":workspaceSlug/projects/:projectId/archives/issues/:archivedIssueId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx"
),
]
// Issue Detail
route(
":workspaceSlug/projects/:projectId/issues/:issueId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx"
),
// Project Archives - Cycles
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx", [
// Cycle Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/archives/cycles",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx"
":workspaceSlug/projects/:projectId/cycles/:cycleId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx"
),
]),
// Project Archives - Modules
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx", [
// Cycles List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/archives/modules",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx"
":workspaceSlug/projects/:projectId/cycles",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx"
),
]),
// Module Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/modules/:moduleId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx"
),
]),
// Modules List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/modules",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx"
),
]),
// View Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/views/:viewId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx"
),
]),
// Views List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/views",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx"
),
]),
// Page Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/pages/:pageId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx"
),
]),
// Pages List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/pages",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx"
),
]),
// Intake list
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/intake",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx"
),
]),
]),
// Project Archives - Issues, Cycles, Modules
// Project Archives - Issues - List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/archives/issues",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx"
),
]),
// Project Archives - Issues - Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/archives/issues/:archivedIssueId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx"
),
]),
// Project Archives - Cycles
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/archives/cycles",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx"
),
]),
// Project Archives - Modules
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/archives/modules",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx"
),
]),
]),
@ -320,44 +311,46 @@ export const coreRoutes: RouteConfigEntry[] = [
// --------------------------------------------------------------------
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx", [
// CORE Routes
// Project Settings
// No Projects available page
route(":workspaceSlug/settings/projects", "./(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx"),
route(
":workspaceSlug/settings/projects/:projectId",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx"
),
// Project Members
route(
":workspaceSlug/settings/projects/:projectId/members",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx"
),
// Project Features
route(
":workspaceSlug/settings/projects/:projectId/features",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx"
),
// Project States
route(
":workspaceSlug/settings/projects/:projectId/states",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx"
),
// Project Labels
route(
":workspaceSlug/settings/projects/:projectId/labels",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx"
),
// Project Estimates
route(
":workspaceSlug/settings/projects/:projectId/estimates",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx"
),
// Project Automations
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/layout.tsx", [
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx", [
// Project Settings
route(
":workspaceSlug/settings/projects/:projectId/automations",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx"
":workspaceSlug/settings/projects/:projectId",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx"
),
// Project Members
route(
":workspaceSlug/settings/projects/:projectId/members",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx"
),
// Project Features
route(
":workspaceSlug/settings/projects/:projectId/features",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx"
),
// Project States
route(
":workspaceSlug/settings/projects/:projectId/states",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx"
),
// Project Labels
route(
":workspaceSlug/settings/projects/:projectId/labels",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx"
),
// Project Estimates
route(
":workspaceSlug/settings/projects/:projectId/estimates",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx"
),
// Project Automations
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/layout.tsx", [
route(
":workspaceSlug/settings/projects/:projectId/automations",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx"
),
]),
]),
]),
]),

View file

@ -1,11 +1,10 @@
import type { FC } from "react";
import { observer } from "mobx-react";
// layouts
import { ProjectAuthWrapper as CoreProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
export type IProjectAuthWrapper = {
workspaceSlug: string;
projectId?: string;
projectId: string;
children: React.ReactNode;
};

View file

@ -1,67 +0,0 @@
import { useState } from "react";
import { useParams } from "next/navigation";
import { ClipboardList } from "lucide-react";
// plane imports
import { Button } from "@plane/propel/button";
// assets
import Unauthorized from "@/app/assets/auth/unauthorized.svg?url";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
type Props = {
projectId?: string;
isPrivateProject?: boolean;
};
export function JoinProject(props: Props) {
const { projectId, isPrivateProject = false } = props;
// states
const [isJoiningProject, setIsJoiningProject] = useState(false);
// store hooks
const { joinProject } = useUserPermissions();
const { fetchProjectDetails } = useProject();
const { workspaceSlug } = useParams();
const handleJoin = () => {
if (!workspaceSlug || !projectId) return;
setIsJoiningProject(true);
joinProject(workspaceSlug.toString(), projectId.toString())
.then(() => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()))
.finally(() => setIsJoiningProject(false));
};
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
<div className="h-44 w-72">
<img src={Unauthorized} className="h-[176px] w-[288px] object-contain" alt="JoinProject" />
</div>
<h1 className="text-xl font-medium text-custom-text-100">
{!isPrivateProject ? `You are not a member of this project yet.` : `You are not a member of this project.`}
</h1>
<div className="w-full max-w-md text-base text-custom-text-200">
<p className="mx-auto w-full text-sm md:w-3/4">
{!isPrivateProject
? `Click the button below to join it.`
: `This is a private project. \n We can't tell you more about this project to protect confidentiality.`}
</p>
</div>
{!isPrivateProject && (
<div>
<Button
variant="primary"
prependIcon={<ClipboardList color="white" />}
loading={isJoiningProject}
onClick={handleJoin}
>
{isJoiningProject ? "Taking you in" : "Click to join"}
</Button>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,70 @@
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
type TProps = {
isWorkspaceAdmin: boolean;
handleJoinProject: () => void;
isJoinButtonDisabled: boolean;
errorStatusCode: number | undefined;
};
export const ProjectAccessRestriction = observer(function ProjectAccessRestriction(props: TProps) {
const { isWorkspaceAdmin, handleJoinProject, isJoinButtonDisabled, errorStatusCode } = props;
// plane hooks
const { t } = useTranslation();
// Show join project screen if:
// - User lacks project membership (409 Conflict)
// - User lacks permission to access the private project (403 Forbidden) but is a workspace admin (can join any project)
if (errorStatusCode === 409 || (errorStatusCode === 403 && isWorkspaceAdmin))
return (
<div className="grid h-full w-full place-items-center bg-custom-background-100">
<EmptyStateDetailed
title={t("project_empty_state.no_access.title")}
description={t("project_empty_state.no_access.join_description")}
assetKey="no-access"
assetClassName="size-40"
actions={[
{
label: isJoinButtonDisabled
? t("project_empty_state.no_access.cta_loading")
: t("project_empty_state.no_access.cta_primary"),
onClick: handleJoinProject,
disabled: isJoinButtonDisabled,
},
]}
/>
</div>
);
// Show no access screen if:
// - User lacks permission to access the private project (403 Forbidden)
if (errorStatusCode === 403) {
return (
<div className="grid h-full w-full place-items-center bg-custom-background-100">
<EmptyStateDetailed
title={t("project_empty_state.no_access.title")}
description={t("project_empty_state.no_access.restricted_description")}
assetKey="no-access"
assetClassName="size-40"
/>
</div>
);
}
// Show empty state screen if:
// - Project not found (404 Not Found)
// - Any other error status code
return (
<div className="grid h-full w-full place-items-center bg-custom-background-100">
<EmptyStateDetailed
title={t("project_empty_state.invalid_project.title")}
description={t("project_empty_state.invalid_project.description")}
assetKey="project"
assetClassName="size-40"
/>
</div>
);
});

View file

@ -6,7 +6,6 @@ import useSWR from "swr";
import { ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
import { EIssuesStoreType } from "@plane/types";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
// hooks
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
@ -26,7 +25,7 @@ export const ArchivedIssueLayoutRoot = observer(function ArchivedIssueLayoutRoot
// derived values
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
@ -36,15 +35,7 @@ export const ArchivedIssueLayoutRoot = observer(function ArchivedIssueLayoutRoot
{ revalidateIfStale: false, revalidateOnFocus: false }
);
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !workItemFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
if (!workspaceSlug || !projectId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.ARCHIVED}>
<ProjectLevelWorkItemFiltersHOC

View file

@ -7,7 +7,6 @@ import useSWR from "swr";
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { TransferIssues } from "@/components/cycles/transfer-issues";
import { TransferIssuesModal } from "@/components/cycles/transfer-issues-modal";
// hooks
@ -59,7 +58,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() {
const workItemFilters = cycleId ? issuesFilter?.getIssueFilters(cycleId) : undefined;
const activeLayout = workItemFilters?.displayFilters?.layout;
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_${workspaceSlug}_${projectId}_${cycleId}` : null,
async () => {
if (workspaceSlug && projectId && cycleId) {
@ -78,15 +77,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() {
: 0;
const canTransferIssues = isProgressSnapshotEmpty && transferableIssuesCount > 0;
if (!workspaceSlug || !projectId || !cycleId) return <></>;
if (isLoading && !workItemFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
if (!workspaceSlug || !projectId || !cycleId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.CYCLE}>
<ProjectLevelWorkItemFiltersHOC

View file

@ -6,8 +6,6 @@ import useSWR from "swr";
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
import { Row, ERowVariant } from "@plane/ui";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
// hooks
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
@ -50,7 +48,7 @@ export const ModuleLayoutRoot = observer(function ModuleLayoutRoot() {
const workItemFilters = moduleId ? issuesFilter?.getIssueFilters(moduleId) : undefined;
const activeLayout = workItemFilters?.displayFilters?.layout || undefined;
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId && moduleId
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
: null,
@ -62,15 +60,7 @@ export const ModuleLayoutRoot = observer(function ModuleLayoutRoot() {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
if (!workspaceSlug || !projectId || !moduleId) return <></>;
if (isLoading && !workItemFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
if (!workspaceSlug || !projectId || !moduleId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.MODULE}>
<ProjectLevelWorkItemFiltersHOC

View file

@ -1,4 +1,3 @@
import type { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
@ -7,7 +6,6 @@ import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@p
import { EIssueLayoutTypes, EIssuesStoreType } from "@plane/types";
import { Spinner } from "@plane/ui";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
// hooks
@ -49,7 +47,7 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() {
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
const activeLayout = workItemFilters?.displayFilters?.layout;
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId) {
@ -59,15 +57,7 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !workItemFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
if (!workspaceSlug || !projectId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT}>
<ProjectLevelWorkItemFiltersHOC

View file

@ -5,8 +5,6 @@ import useSWR from "swr";
// plane constants
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
// hooks
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
@ -60,7 +58,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() {
}
: undefined;
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null,
async () => {
if (workspaceSlug && projectId && viewId) {
@ -78,16 +76,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() {
[issuesFilter, workspaceSlug, viewId]
);
if (!workspaceSlug || !projectId || !viewId) return <></>;
if (isLoading && !workItemFilters) {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
}
if (!workspaceSlug || !projectId || !viewId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT_VIEW}>
<ProjectLevelWorkItemFiltersHOC

View file

@ -6,7 +6,6 @@ import { SPREADSHEET_SELECT_GROUP, SPREADSHEET_PROPERTY_LIST } from "@plane/cons
import type { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
import { EIssueLayoutTypes } from "@plane/types";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { MultipleSelectGroup } from "@/components/core/multiple-select";
// hooks
import { useProject } from "@/hooks/store/use-project";
@ -72,13 +71,7 @@ export const SpreadsheetView = observer(function SpreadsheetView(props: Props) {
return true;
});
if (!issueIds || issueIds.length === 0)
return (
<div className="grid h-full w-full place-items-center">
<LogoSpinner />
</div>
);
if (!issueIds || issueIds.length === 0) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
<div ref={portalRef} className="spreadsheet-menu-portal" />

View file

@ -17,7 +17,6 @@ import type { IWorkspace } from "@plane/types";
import { CustomSelect, Input } from "@plane/ui";
import { copyUrlToClipboard, getFileURL } from "@plane/utils";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
@ -129,13 +128,7 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
if (!currentWorkspace)
return (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<LogoSpinner />
</div>
);
if (!currentWorkspace) return <></>;
return (
<>
<Controller

View file

@ -1,15 +1,12 @@
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EProjectNetwork, GANTT_TIMELINE_TYPE } from "@plane/types";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { GANTT_TIMELINE_TYPE } from "@plane/types";
// components
import { JoinProject } from "@/components/auth-screens/project/join-project";
import { LogoSpinner } from "@/components/common/logo-spinner";
import { ProjectAccessRestriction } from "@/components/auth-screens/project/project-access-restriction";
import {
PROJECT_DETAILS,
PROJECT_ME_INFORMATION,
@ -23,10 +20,8 @@ import {
PROJECT_VIEWS,
PROJECT_INTAKE_STATE,
} from "@/constants/fetch-keys";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useProjectEstimates } from "@/hooks/store/estimates";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useCycle } from "@/hooks/store/use-cycle";
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
@ -39,19 +34,19 @@ import { useTimeLineChart } from "@/hooks/use-timeline-chart";
interface IProjectAuthWrapper {
workspaceSlug: string;
projectId?: string;
projectId: string;
children: ReactNode;
isLoading?: boolean;
}
export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IProjectAuthWrapper) {
const { workspaceSlug, projectId, children, isLoading: isParentLoading = false } = props;
// plane hooks
const { t } = useTranslation();
// states
const [isJoiningProject, setIsJoiningProject] = useState(false);
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { fetchUserProjectInfo, allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
const { loader, getProjectById, fetchProjectDetails } = useProject();
const { fetchUserProjectInfo, allowPermissions } = useUserPermissions();
const { fetchProjectDetails } = useProject();
const { joinProject } = useUserPermissions();
const { fetchAllCycles } = useCycle();
const { fetchModulesSlim, fetchModules } = useModule();
const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE);
@ -63,10 +58,7 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
const { data: currentUserData } = useUser();
const { fetchProjectLabels } = useLabel();
const { getProjectEstimates } = useProjectEstimates();
// derived values
const projectExists = projectId ? getProjectById(projectId) : null;
const projectMemberInfo = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
const hasPermissionToCurrentProject = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
EUserPermissionsLevel.PROJECT,
@ -82,120 +74,84 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
}, []);
// fetching project details
useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug, projectId) : null
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(
workspaceSlug && projectId && currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(workspaceSlug, projectId) : null,
workspaceSlug && projectId && currentUserData?.id
? () => fetchProjectMemberPreferences(workspaceSlug, projectId, currentUserData.id)
: null,
currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(workspaceSlug, projectId) : null,
currentUserData?.id ? () => fetchProjectMemberPreferences(workspaceSlug, projectId, currentUserData.id) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project labels
useSWR(
workspaceSlug && projectId ? PROJECT_LABELS(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(PROJECT_LABELS(workspaceSlug, projectId), () => fetchProjectLabels(workspaceSlug, projectId), {
revalidateIfStale: false,
revalidateOnFocus: false,
});
// fetching project members
useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(PROJECT_MEMBERS(workspaceSlug, projectId), () => fetchProjectMembers(workspaceSlug, projectId), {
revalidateIfStale: false,
revalidateOnFocus: false,
});
// fetching project states
useSWR(
workspaceSlug && projectId ? PROJECT_STATES(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(PROJECT_STATES(workspaceSlug, projectId), () => fetchProjectStates(workspaceSlug, projectId), {
revalidateIfStale: false,
revalidateOnFocus: false,
});
// fetching project intake state
useSWR(
workspaceSlug && projectId ? PROJECT_INTAKE_STATE(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchProjectIntakeState(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(PROJECT_INTAKE_STATE(workspaceSlug, projectId), () => fetchProjectIntakeState(workspaceSlug, projectId), {
revalidateIfStale: false,
revalidateOnFocus: false,
});
// fetching project estimates
useSWR(
workspaceSlug && projectId ? PROJECT_ESTIMATES(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => getProjectEstimates(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(PROJECT_ESTIMATES(workspaceSlug, projectId), () => getProjectEstimates(workspaceSlug, projectId), {
revalidateIfStale: false,
revalidateOnFocus: false,
});
// fetching project cycles
useSWR(
workspaceSlug && projectId ? PROJECT_ALL_CYCLES(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(PROJECT_ALL_CYCLES(workspaceSlug, projectId), () => fetchAllCycles(workspaceSlug, projectId), {
revalidateIfStale: false,
revalidateOnFocus: false,
});
// fetching project modules
useSWR(
workspaceSlug && projectId ? PROJECT_MODULES(workspaceSlug, projectId) : null,
workspaceSlug && projectId
? async () => {
await fetchModulesSlim(workspaceSlug, projectId);
await fetchModules(workspaceSlug, projectId);
}
: null,
PROJECT_MODULES(workspaceSlug, projectId),
async () => {
await Promise.all([fetchModulesSlim(workspaceSlug, projectId), fetchModules(workspaceSlug, projectId)]);
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project views
useSWR(
workspaceSlug && projectId ? PROJECT_VIEWS(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchViews(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(PROJECT_VIEWS(workspaceSlug, projectId), () => fetchViews(workspaceSlug, projectId), {
revalidateIfStale: false,
revalidateOnFocus: false,
});
// permissions
const canPerformEmptyStateActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
// handle join project
const handleJoinProject = () => {
setIsJoiningProject(true);
joinProject(workspaceSlug, projectId)
.then(() => fetchProjectDetails(workspaceSlug, projectId))
.finally(() => setIsJoiningProject(false));
};
// check if the project member apis is loading
if (isParentLoading || (!projectMemberInfo && projectId && hasPermissionToCurrentProject === null))
const isProjectLoading = (isParentLoading || isProjectDetailsLoading) && !projectDetailsError;
if (isProjectLoading) return null;
if (!isProjectLoading && hasPermissionToCurrentProject === false) {
return (
<div className="grid h-full place-items-center bg-custom-background-100 p-4 rounded-lg border border-custom-border-200">
<div className="flex flex-col items-center gap-3 text-center">
<LogoSpinner />
</div>
</div>
);
// check if the user don't have permission to access the project
if (
((projectExists?.network && projectExists?.network !== EProjectNetwork.PRIVATE) || isWorkspaceAdmin) &&
projectId &&
hasPermissionToCurrentProject === false
)
return <JoinProject projectId={projectId} />;
// check if the project info is not found.
if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false)
return (
<div className="grid h-full place-items-center bg-custom-background-100">
<EmptyStateDetailed
title={t("workspace_projects.empty_state.general.title")}
description={t("workspace_projects.empty_state.general.description")}
assetKey="project"
assetClassName="size-40"
actions={[
{
label: t("workspace_projects.empty_state.general.primary_button.text"),
onClick: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
},
disabled: !canPerformEmptyStateActions,
variant: "primary",
},
]}
/>
</div>
<ProjectAccessRestriction
errorStatusCode={projectDetailsError?.status}
isWorkspaceAdmin={isWorkspaceAdmin}
handleJoinProject={handleJoinProject}
isJoinButtonDisabled={isJoiningProject}
/>
);
}
return <>{children}</>;
});

View file

@ -57,7 +57,7 @@ export class ProjectService extends APIService {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
throw error?.response;
});
}