[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;
});
}

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Vypadá to, že nemáte přístup k tomuto projektu",
restricted_description: "Kontaktujte administrátora a požádejte o přístup, abyste zde mohli pokračovat.",
join_description: "Klikněte na tlačítko níže pro připojení k projektu.",
cta_primary: "Připojit se k projektu",
cta_loading: "Připojování k projektu",
},
invalid_project: {
title: "Projekt nebyl nalezen",
description: "Projekt, který hledáte, neexistuje.",
},
work_items: {
title: "Začněte s vaší první pracovní položkou.",
description:

View file

@ -26,6 +26,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Es scheint, als hätten Sie keinen Zugriff auf dieses Projekt",
restricted_description: "Kontaktieren Sie den Administrator, um Zugriff anzufordern, damit Sie hier fortfahren können.",
join_description: "Klicken Sie unten auf die Schaltfläche, um beizutreten.",
cta_primary: "Projekt beitreten",
cta_loading: "Projekt wird beigetreten",
},
invalid_project: {
title: "Projekt nicht gefunden",
description: "Das gesuchte Projekt existiert nicht.",
},
work_items: {
title: "Beginnen Sie mit Ihrem ersten Arbeitselement.",
description:

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Seems like you dont have access to this Project",
restricted_description: "Contact admin to request for access and you can continue here.",
join_description: "Click the button below to join it.",
cta_primary: "Join project",
cta_loading: "Joining project",
},
invalid_project: {
title: "Project not found",
description: "The project you are looking for does not exist.",
},
work_items: {
title: "Start with your first work item.",
description:

View file

@ -26,6 +26,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Parece que no tienes acceso a este proyecto",
restricted_description: "Contacta con el administrador para solicitar acceso y podrás continuar aquí.",
join_description: "Haz clic en el botón de abajo para unirte.",
cta_primary: "Unirse al proyecto",
cta_loading: "Uniéndose al proyecto",
},
invalid_project: {
title: "Proyecto no encontrado",
description: "El proyecto que buscas no existe.",
},
work_items: {
title: "Comienza con tu primer elemento de trabajo.",
description:

View file

@ -27,6 +27,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Il semble que vous nayez pas accès à ce projet",
restricted_description: "Contactez ladministrateur pour demander laccès afin de pouvoir continuer ici.",
join_description: "Cliquez sur le bouton ci-dessous pour rejoindre le projet.",
cta_primary: "Rejoindre le projet",
cta_loading: "Rejoindre le projet…",
},
invalid_project: {
title: "Projet non trouvé",
description: "Le projet que vous recherchez nexiste pas.",
},
work_items: {
title: "Commencez avec votre premier élément de travail.",
description:

View file

@ -25,6 +25,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Sepertinya Anda tidak memiliki akses ke Proyek ini",
restricted_description: "Hubungi admin untuk meminta akses agar Anda dapat melanjutkan di sini.",
join_description: "Klik tombol di bawah ini untuk bergabung.",
cta_primary: "Bergabung dengan proyek",
cta_loading: "Sedang bergabung dengan proyek",
},
invalid_project: {
title: "Proyek tidak ditemukan",
description: "Proyek yang Anda cari tidak ada.",
},
work_items: {
title: "Mulai dengan item kerja pertama Anda.",
description:

View file

@ -27,6 +27,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Sembra che tu non abbia accesso a questo progetto",
restricted_description: "Contatta l'amministratore per richiedere l'accesso e potrai continuare qui.",
join_description: "Clicca sul pulsante qui sotto per unirti.",
cta_primary: "Unisciti al progetto",
cta_loading: "Unione al progetto in corso",
},
invalid_project: {
title: "Progetto non trovato",
description: "Il progetto che stai cercando non esiste.",
},
work_items: {
title: "Inizia con il tuo primo elemento di lavoro.",
description:

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "このプロジェクトへのアクセス権がないようです",
restricted_description: "管理者に連絡してアクセス権をリクエストすると、ここで作業を続けられます。",
join_description: "下のボタンをクリックして参加してください。",
cta_primary: "プロジェクトに参加",
cta_loading: "プロジェクトに参加中",
},
invalid_project: {
title: "プロジェクトが見つかりません",
description: "お探しのプロジェクトは存在しません。",
},
work_items: {
title: "最初の作業項目から始めましょう。",
description:

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "이 프로젝트에 접근할 수 없는 것 같습니다",
restricted_description: "관리자에게 접근 권한을 요청하시면 여기서 계속 진행하실 수 있습니다.",
join_description: "아래 버튼을 클릭하여 프로젝트에 참여하세요.",
cta_primary: "프로젝트 참여",
cta_loading: "프로젝트 참여 중",
},
invalid_project: {
title: "프로젝트를 찾을 수 없습니다",
description: "찾으시는 프로젝트가 존재하지 않습니다.",
},
work_items: {
title: "첫 번째 작업 항목으로 시작하세요.",
description:

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Wygląda na to, że nie masz dostępu do tego projektu",
restricted_description: "Skontaktuj się z administratorem, aby poprosić o dostęp i móc kontynuować tutaj.",
join_description: "Kliknij przycisk poniżej, aby dołączyć.",
cta_primary: "Dołącz do projektu",
cta_loading: "Dołączanie do projektu",
},
invalid_project: {
title: "Projekt nie został znaleziony",
description: "Projekt, którego szukasz, nie istnieje.",
},
work_items: {
title: "Zacznij od swojego pierwszego elementu roboczego.",
description:

View file

@ -26,6 +26,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Parece que você não tem acesso a este projeto",
restricted_description: "Entre em contato com o administrador para solicitar acesso e você poderá continuar aqui.",
join_description: "Clique no botão abaixo para participar.",
cta_primary: "Participar do projeto",
cta_loading: "Participando do projeto",
},
invalid_project: {
title: "Projeto não encontrado",
description: "O projeto que você está procurando não existe.",
},
work_items: {
title: "Comece com seu primeiro item de trabalho.",
description:

View file

@ -25,6 +25,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Se pare că nu aveți acces la acest proiect",
restricted_description: "Contactați administratorul pentru a solicita accesul și veți putea continua aici.",
join_description: "Faceți clic pe butonul de mai jos pentru a vă alătura.",
cta_primary: "Alăturați-vă proiectului",
cta_loading: "Se alătură proiectului",
},
invalid_project: {
title: "Proiect negăsit",
description: "Proiectul pe care îl căutați nu există.",
},
work_items: {
title: "Începeți cu primul dvs. element de lucru.",
description:

View file

@ -25,6 +25,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Похоже, у вас нет доступа к этому проекту",
restricted_description: "Свяжитесь с администратором, чтобы запросить доступ, и вы сможете продолжить здесь.",
join_description: "Нажмите кнопку ниже, чтобы присоединиться.",
cta_primary: "Присоединиться к проекту",
cta_loading: "Присоединение к проекту",
},
invalid_project: {
title: "Проект не найден",
description: "Проект, который вы ищете, не существует.",
},
work_items: {
title: "Начните с вашего первого рабочего элемента.",
description:

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Zdá sa, že nemáte prístup k tomuto projektu",
restricted_description: "Kontaktujte administrátora, aby ste požiadali o prístup, a potom tu môžete pokračovať.",
join_description: "Kliknite na tlačidlo nižšie, aby ste sa pripojili.",
cta_primary: "Pripojiť sa k projektu",
cta_loading: "Pripájanie k projektu",
},
invalid_project: {
title: "Projekt nebol nájdený",
description: "Projekt, ktorý hľadáte, neexistuje.",
},
work_items: {
title: "Začnite s vašou prvou pracovnou položkou.",
description:

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Görünüşe göre bu projeye erişiminiz yok",
restricted_description: "Erişim talep etmek için yöneticiyle iletişime geçin, sonra burada devam edebilirsiniz.",
join_description: "Katılmak için aşağıdaki butona tıklayın.",
cta_primary: "Projeye katıl",
cta_loading: "Projeye katılınıyor",
},
invalid_project: {
title: "Proje bulunamadı",
description: "Aradığınız proje mevcut değil.",
},
work_items: {
title: "İlk iş öğenizle başlayın.",
description:

View file

@ -25,6 +25,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Схоже, у вас немає доступу до цього проєкту",
restricted_description: "Зверніться до адміністратора, щоб запросити доступ, і ви зможете продовжити тут.",
join_description: "Натисніть кнопку нижче, щоб приєднатися.",
cta_primary: "Приєднатися до проєкту",
cta_loading: "Приєднання до проєкту",
},
invalid_project: {
title: "Проєкт не знайдено",
description: "Проєкт, який ви шукаєте, не існує.",
},
work_items: {
title: "Почніть з вашого першого робочого елемента.",
description:

View file

@ -25,6 +25,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "Có vẻ như bạn không có quyền truy cập vào Dự án này",
restricted_description: "Liên hệ với quản trị viên để yêu cầu quyền truy cập và bạn có thể tiếp tục tại đây.",
join_description: "Nhấn nút bên dưới để tham gia.",
cta_primary: "Tham gia dự án",
cta_loading: "Đang tham gia dự án",
},
invalid_project: {
title: "Không tìm thấy dự án",
description: "Dự án bạn đang tìm kiếm không tồn tại.",
},
work_items: {
title: "Bắt đầu với mục công việc đầu tiên của bạn.",
description:

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "您似乎无权访问该项目",
restricted_description: "请联系管理员申请访问权限,通过后您可以在此继续。",
join_description: "点击下方按钮加入该项目。",
cta_primary: "加入项目",
cta_loading: "正在加入项目",
},
invalid_project: {
title: "未找到项目",
description: "您查找的项目不存在。",
},
work_items: {
title: "从您的第一个工作项开始。",
description: "工作项是项目的构建块 — 分配负责人、设置优先级并轻松跟踪进度。",

View file

@ -24,6 +24,17 @@ export default {
},
},
project_empty_state: {
no_access: {
title: "您似乎無權存取此專案",
restricted_description: "請聯絡管理員以申請存取權,核准後即可在此繼續。",
join_description: "點擊下方按鈕加入專案。",
cta_primary: "加入專案",
cta_loading: "正在加入專案",
},
invalid_project: {
title: "找不到專案",
description: "您所查找的專案不存在。",
},
work_items: {
title: "從您的第一個工作項開始。",
description: "工作項是專案的建構模組 — 指派負責人、設定優先順序並輕鬆追蹤進度。",