[WEB-4334] refactor: project layout loader and rendering conditions (#4841)

* 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: update project layout routes and improve rendering conditions

- Modified project detail layout path to include projectId.
- Refactored settings layout to streamline project-related routes.
- Enhanced rendering conditions in various components to prevent unnecessary rendering when required data is missing.

* 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.

* refactor: streamline issue details rendering and improve error handling

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

* refactor: simplify SWR hooks in ProjectAuthWrapper for better readability

* fix: teamspace member check in project retriev api

---------

Co-authored-by: sangeethailango <[email protected]>
Co-authored-by: Dheeraj Kumar Ketireddy <[email protected]>
Co-authored-by: sriram veeraghanta <[email protected]>
This commit is contained in:
Prateek Shourya
2025-12-01 17:10:33 +05:30
committed by GitHub
co-authored by sangeethailango Dheeraj Kumar Ketireddy sriram veeraghanta
parent 4870a6e578
commit dde559dfc4
50 changed files with 790 additions and 727 deletions
+41 -21
View File
@@ -1,38 +1,42 @@
# Python imports
from django.utils import timezone
import json
from typing import Dict, Any, List
import uuid
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 import IntegrityError
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,
UserFavorite,
DEFAULT_STATES,
Workspace,
WorkspaceMember,
@@ -360,21 +364,37 @@ class ProjectViewSet(BaseViewSet):
def retrieve(self, request, slug, pk):
project = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
# filter projects
project = project.filter(
Q(pk__in=self.get_teamspace_project_ids(request, slug))
| Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
)
# projects
project = project.first()
if project is None:
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
# Check if the user is either part of the project or the teamspace
# Get all the member ids of the project
member_ids = [str(project_member.member_id) for project_member in project.members_list]
project_teamspaces_ids = TeamspaceProject.objects.filter(workspace__slug=slug, project_id=pk).values_list(
"team_space_id"
)
is_teamspace_member = TeamspaceMember.objects.filter(
member_id=request.user.id, team_space_id__in=project_teamspaces_ids
).exists()
is_project_member = str(request.user.id) in member_ids
# Return error message based on the project network
if not (is_project_member or is_teamspace_member):
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,
+1
View File
@@ -52,6 +52,7 @@ from .project import (
ProjectIdentifier,
ProjectMember,
ProjectMemberInvite,
ProjectNetwork,
ProjectPublicMember,
)
from .session import Session
@@ -11,7 +11,6 @@ import { Loader } from "@plane/ui";
import emptyIssueDark from "@/app/assets/empty-state/search/issues-dark.webp?url";
import emptyIssueLight from "@/app/assets/empty-state/search/issues-light.webp?url";
// components
import { JoinProject } from "@/components/auth-screens/project/join-project";
import { EmptyState } from "@/components/common/empty-state";
import { PageHead } from "@/components/core/page-title";
import { IssueDetailRoot } from "@/components/issues/issue-detail";
@@ -88,69 +87,63 @@ function IssueDetailsPage({ params }: Route.ComponentProps) {
}
}, [workspaceSlug, data]);
const renderPrivateProjectEmptyState = error && error.type === 0;
return (
<>
<PageHead title={pageTitle} />
{renderPrivateProjectEmptyState ? (
<JoinProject projectId={projectId} isPrivateProject />
) : (
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
{error ? (
<EmptyState
image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight}
title={t("issue.empty_state.issue_detail.title")}
description={t("issue.empty_state.issue_detail.description")}
primaryButton={{
text: t("issue.empty_state.issue_detail.primary_button.text"),
onClick: () => router.push(`/${workspaceSlug}/workspace-views/all-issues/`),
}}
/>
) : issueLoader ? (
<Loader className="flex h-full gap-5 p-5">
<div className="basis-2/3 space-y-2">
<Loader.Item height="30px" width="40%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="40%" />
</div>
<div className="basis-1/3 space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
) : (
workspaceSlug &&
projectId &&
issueId && (
<>
{issue?.is_epic ? (
<>
<EpicDetailRoot
editorRef={editorRef}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
epicId={issueId.toString()}
/>
</>
) : (
<>
<IssueDetailRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={issueId.toString()}
is_archived={!!issue?.archived_at}
/>
</>
)}
</>
)
)}
</ProjectAuthWrapper>
)}
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
{error && !issueLoader ? (
<EmptyState
image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight}
title={t("issue.empty_state.issue_detail.title")}
description={t("issue.empty_state.issue_detail.description")}
primaryButton={{
text: t("issue.empty_state.issue_detail.primary_button.text"),
onClick: () => router.push(`/${workspaceSlug}/workspace-views/all-issues/`),
}}
/>
) : issueLoader ? (
<Loader className="flex h-full gap-5 p-5">
<div className="basis-2/3 space-y-2">
<Loader.Item height="30px" width="40%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="40%" />
</div>
<div className="basis-1/3 space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
) : (
workspaceSlug &&
projectId &&
issueId && (
<>
{issue?.is_epic ? (
<>
<EpicDetailRoot
editorRef={editorRef}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
epicId={issueId.toString()}
/>
</>
) : (
<>
<IssueDetailRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={issueId.toString()}
is_archived={!!issue?.archived_at}
/>
</>
)}
</>
)
)}
</ProjectAuthWrapper>
</>
);
}
@@ -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>
</>
);
}
@@ -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>
);
}
@@ -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")}`} />
@@ -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>
</>
);
}
@@ -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);
@@ -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}
@@ -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);
@@ -1,10 +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";
import noProjectsDark from "@/app/assets/empty-state/project-settings/no-projects-dark.png?url";
import noProjectsLight from "@/app/assets/empty-state/project-settings/no-projects-light.png?url";
// 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() {
@@ -12,11 +16,10 @@ function ProjectSettingsPage() {
const { resolvedTheme } = useTheme();
const { toggleCreateProjectModal } = useCommandPalette();
// derived values
const resolvedPath = resolvedTheme === "dark" ? noProjectsDark : noProjectsLight;
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);
+149 -156
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"
),
]),
]),
]),
]),
+96 -93
View File
@@ -179,46 +179,44 @@ export const extendedRoutes: RouteConfigEntry[] = [
// PROJECT LEVEL ROUTES
// --------------------------------------------------------------------
// Project Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx", [
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [
// Project Overview
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/overview/(list)/layout.tsx", [
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [
// Project Overview
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/overview/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/overview",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/overview/(list)/page.tsx"
),
]),
// Project Epics
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/layout.tsx", [
// Epics List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/overview",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/overview/(list)/page.tsx"
":workspaceSlug/projects/:projectId/epics",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(list)/page.tsx"
),
]),
// Project Epics
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/layout.tsx", [
// Epics List
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(list)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/epics",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(list)/page.tsx"
),
]),
// Project Epic Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/epics/:epicId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/[epicId]/page.tsx"
),
]),
// Project Epic Detail
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/layout.tsx", [
route(
":workspaceSlug/projects/:projectId/epics/:epicId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/[epicId]/page.tsx"
),
]),
// Project Automations
layout(
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/layout.tsx",
[
route(
":workspaceSlug/projects/:projectId/automations/:automationId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/page.tsx"
),
]
),
]),
// Project Automations
layout(
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/layout.tsx",
[
route(
":workspaceSlug/projects/:projectId/automations/:automationId",
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/page.tsx"
),
]
),
]),
// Pi Chat (Project Level)
@@ -477,76 +475,81 @@ export const extendedRoutes: RouteConfigEntry[] = [
// --------------------------------------------------------------------
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx", [
// Project Epics
route(
":workspaceSlug/settings/projects/:projectId/epics",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/epics/page.tsx"
),
// Project Integrations
route(
":workspaceSlug/settings/projects/:projectId/integrations",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/integrations/page.tsx"
),
// Project Updates
route(
":workspaceSlug/settings/projects/:projectId/project-updates",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/project-updates/page.tsx"
),
// Project Work Item Types
route(
":workspaceSlug/settings/projects/:projectId/work-item-types",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/work-item-types/page.tsx"
),
// Project Workflows
route(
":workspaceSlug/settings/projects/:projectId/workflows",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/workflows/page.tsx"
),
// Project Features - Cycles
route(
":workspaceSlug/settings/projects/:projectId/features/cycles",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx"
),
// Project Features - Intake
route(
":workspaceSlug/settings/projects/:projectId/features/intake",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx"
),
// Recurring Work Items
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/layout.tsx", [
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx", [
// Project Epics
route(
":workspaceSlug/settings/projects/:projectId/recurring-work-items",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/page.tsx"
":workspaceSlug/settings/projects/:projectId/epics",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/epics/page.tsx"
),
// Project Integrations
route(
":workspaceSlug/settings/projects/:projectId/integrations",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/integrations/page.tsx"
),
// Project Updates
route(
":workspaceSlug/settings/projects/:projectId/project-updates",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/project-updates/page.tsx"
),
// Project Work Item Types
route(
":workspaceSlug/settings/projects/:projectId/work-item-types",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/work-item-types/page.tsx"
),
// Project Workflows
route(
":workspaceSlug/settings/projects/:projectId/workflows",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/workflows/page.tsx"
),
// Project Features - Cycles
route(
":workspaceSlug/settings/projects/:projectId/features/cycles",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx"
),
// Project Features - Intake
route(
":workspaceSlug/settings/projects/:projectId/features/intake",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx"
),
// Recurring Work Items
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/layout.tsx", [
route(
":workspaceSlug/settings/projects/:projectId/recurring-work-items",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/page.tsx"
),
layout(
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/(details)/layout.tsx",
[
route(
":workspaceSlug/settings/projects/:projectId/recurring-work-items/create",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/(details)/create/page.tsx"
),
route(
":workspaceSlug/settings/projects/:projectId/recurring-work-items/:recurringWorkItemId/update",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/(details)/[recurringWorkItemId]/update/page.tsx"
),
]
),
]),
// Project Templates
route(
":workspaceSlug/settings/projects/:projectId/templates",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/page.tsx"
),
layout(
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/(details)/layout.tsx",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/(templates)/layout.tsx",
[
route(
":workspaceSlug/settings/projects/:projectId/recurring-work-items/create",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/(details)/create/page.tsx"
":workspaceSlug/settings/projects/:projectId/templates/page",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/(templates)/page/page.tsx"
),
route(
":workspaceSlug/settings/projects/:projectId/recurring-work-items/:recurringWorkItemId/update",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/recurring-work-items/(details)/[recurringWorkItemId]/update/page.tsx"
":workspaceSlug/settings/projects/:projectId/templates/work-item",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/(templates)/work-item/page.tsx"
),
]
),
]),
// Project Templates
route(
":workspaceSlug/settings/projects/:projectId/templates",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/page.tsx"
),
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/(templates)/layout.tsx", [
route(
":workspaceSlug/settings/projects/:projectId/templates/page",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/(templates)/page/page.tsx"
),
route(
":workspaceSlug/settings/projects/:projectId/templates/work-item",
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/templates/(templates)/work-item/page.tsx"
),
]),
]),
]),
]),
+1 -2
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;
};
@@ -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>
);
}
@@ -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>
);
});
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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" />
@@ -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
@@ -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}</>;
});
@@ -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;
});
}
@@ -73,7 +73,7 @@ export const ProjectEpicsLayoutRoot: FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
if (!workspaceSlug || !projectId) return <></>;
if (!workspaceSlug || !projectId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.EPIC}>
@@ -9,7 +9,6 @@ import { ISSUE_DISPLAY_FILTERS_BY_PAGE, TEAMSPACE_VIEW_TRACKER_ELEMENTS } from "
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
import { Spinner } from "@plane/ui";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { CalendarLayout } from "@/components/issues/issue-layouts/calendar/roots/project-root";
import { KanBanLayout } from "@/components/issues/issue-layouts/kanban/roots/project-root";
import { ListLayout } from "@/components/issues/issue-layouts/list/roots/project-root";
@@ -72,7 +71,7 @@ export const TeamspaceLayoutRoot: FC = observer(() => {
// fetch all issue properties
useWorkspaceIssueProperties(workspaceSlug);
// fetch teamspace issue filters
const { isLoading } = useSWR(
useSWR(
workspaceSlug && teamspaceId ? `TEAMSPACE_ISSUE_FILTERS_${workspaceSlug}_${teamspaceId}` : null,
async () => {
if (workspaceSlug && teamspaceId) {
@@ -82,15 +81,7 @@ export const TeamspaceLayoutRoot: FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
if (!workspaceSlug || !teamspaceId) return <></>;
if (isLoading && !issuesFilter?.getIssueFilters(teamspaceId))
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
if (!workspaceSlug || !teamspaceId || !issuesFilter?.getIssueFilters(teamspaceId)) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.TEAM}>
<TeamspaceLevelWorkItemFiltersHOC
@@ -8,7 +8,6 @@ import { ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
import { Spinner } from "@plane/ui";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { CalendarLayout } from "@/components/issues/issue-layouts/calendar/roots/project-root";
import { KanBanLayout } from "@/components/issues/issue-layouts/kanban/roots/project-root";
import { ListLayout } from "@/components/issues/issue-layouts/list/roots/project-root";
@@ -54,7 +53,7 @@ export const TeamspaceProjectWorkLayoutRoot: React.FC = observer(() => {
// swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug);
// fetch teamspace view issue filters
const { isLoading } = useSWR(
useSWR(
workspaceSlug && teamspaceId && projectId
? `TEAMSPACE_PROJECT_WORK_ITEMS_ISSUE_FILTERS_${workspaceSlug}_${teamspaceId}_${projectId}`
: null,
@@ -66,16 +65,7 @@ export const TeamspaceProjectWorkLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
if (!workspaceSlug || !teamspaceId || !projectId) return <></>;
if (isLoading && !workItemFilters) {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
}
if (!workspaceSlug || !teamspaceId || !projectId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS}>
{/* TODO: Check if saving a view should be allowed here, as it will create a project-level view. We can't create teamspace views since the filters won't be compatible. */}
@@ -7,7 +7,6 @@ import { ISSUE_DISPLAY_FILTERS_BY_PAGE, TEAMSPACE_VIEW_TRACKER_ELEMENTS } from "
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
import { Spinner } from "@plane/ui";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
// hooks
@@ -61,7 +60,7 @@ export const TeamspaceViewLayoutRoot: React.FC = observer(() => {
// swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug);
// fetch teamspace view issue filters
const { isLoading } = useSWR(
useSWR(
workspaceSlug && teamspaceId && viewId
? `TEAMSPACE_VIEW_ISSUE_FILTERS_${workspaceSlug}_${teamspaceId}_${viewId}`
: null,
@@ -81,16 +80,7 @@ export const TeamspaceViewLayoutRoot: React.FC = observer(() => {
[issuesFilter, workspaceSlug, teamspaceId, viewId]
);
if (!workspaceSlug || !teamspaceId || !viewId) return <></>;
if (isLoading && !workItemFilters) {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
}
if (!workspaceSlug || !teamspaceId || !viewId || !workItemFilters) return <></>;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.TEAM_VIEW}>
<TeamspaceLevelWorkItemFiltersHOC
@@ -3,7 +3,6 @@ import { observer } from "mobx-react";
// plane imports
import { SPREADSHEET_SELECT_GROUP } from "@plane/constants";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { MultipleSelectGroup } from "@/components/core/multiple-select";
// plane web imports
import { SPREADSHEET_PROPERTY_LIST } from "@/plane-web/constants/project/spreadsheet";
@@ -30,12 +29,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
// plane web hooks
const isBulkOperationsEnabled = useBulkOperationStatus();
if (!projectIds || projectIds.length === 0)
return (
<div className="grid h-full w-full place-items-center">
<LogoSpinner />
</div>
);
if (!projectIds || projectIds.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" />
+12 -17
View File
@@ -35,37 +35,32 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
const isMilestonesFeatureEnabled = projectId ? isMilestonesEnabled(workspaceSlug, projectId) : false;
// fetching all work item types and properties
useSWR(
workspaceSlug && projectId && isWorkItemTypeEnabled
? WORK_ITEM_TYPES_PROPERTIES_AND_OPTIONS(workspaceSlug, projectId)
: null,
workspaceSlug && projectId && isWorkItemTypeEnabled
? () => fetchAllWorkItemTypePropertiesAndOptions(workspaceSlug, projectId)
: null,
isWorkItemTypeEnabled ? WORK_ITEM_TYPES_PROPERTIES_AND_OPTIONS(workspaceSlug, projectId) : null,
isWorkItemTypeEnabled ? () => fetchAllWorkItemTypePropertiesAndOptions(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching all epic types and properties
useSWR(
workspaceSlug && projectId && isEpicEnabled ? EPICS_PROPERTIES_AND_OPTIONS(workspaceSlug, projectId) : null,
workspaceSlug && projectId && isEpicEnabled
? () => fetchAllEpicPropertiesAndOptions(workspaceSlug, projectId)
: null,
isEpicEnabled ? EPICS_PROPERTIES_AND_OPTIONS(workspaceSlug, projectId) : null,
isEpicEnabled ? () => fetchAllEpicPropertiesAndOptions(workspaceSlug, projectId) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project level workflow states
useSWR(
workspaceSlug && projectId && isWorkflowFeatureFlagEnabled ? PROJECT_WORKFLOWS(workspaceSlug, projectId) : null,
workspaceSlug && projectId && isWorkflowFeatureFlagEnabled
? () => fetchWorkflowStates(workspaceSlug, projectId)
: null,
{ revalidateIfStale: false, revalidateOnFocus: false }
isWorkflowFeatureFlagEnabled ? PROJECT_WORKFLOWS(workspaceSlug, projectId) : null,
() => fetchWorkflowStates(workspaceSlug, projectId),
{
revalidateIfStale: false,
revalidateOnFocus: false,
}
);
// fetching project level milestones
useSWR(
projectId && workspaceSlug && isMilestonesFeatureEnabled ? `PROJECT_MILESTONES_${projectId}` : null,
projectId && workspaceSlug && isMilestonesFeatureEnabled ? () => fetchMilestones(workspaceSlug, projectId) : null,
isMilestonesFeatureEnabled ? `PROJECT_MILESTONES_${projectId}` : null,
isMilestonesFeatureEnabled ? () => fetchMilestones(workspaceSlug, projectId) : null,
{
revalidateIfStale: false,
revalidateOnFocus: false,
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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: "工作项是项目的构建块 — 分配负责人、设置优先级并轻松跟踪进度。",
@@ -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: "工作項是專案的建構模組 — 指派負責人、設定優先順序並輕鬆追蹤進度。",