[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:
parent
8db95d9ec0
commit
60220801ac
43 changed files with 609 additions and 516 deletions
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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,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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue