[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
|
|
@ -1,43 +1,43 @@
|
||||||
# Python imports
|
# Python imports
|
||||||
import boto3
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import timezone
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
|
||||||
# Django imports
|
# 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.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.views.base import BaseViewSet, BaseAPIView
|
from plane.app.permissions import ROLE, ProjectMemberPermission, allow_permission
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ProjectSerializer,
|
|
||||||
ProjectListSerializer,
|
|
||||||
DeployBoardSerializer,
|
DeployBoardSerializer,
|
||||||
|
ProjectListSerializer,
|
||||||
|
ProjectSerializer,
|
||||||
)
|
)
|
||||||
|
from plane.app.views.base import BaseAPIView, BaseViewSet
|
||||||
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
|
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 (
|
from plane.db.models import (
|
||||||
UserFavorite,
|
|
||||||
Intake,
|
|
||||||
DeployBoard,
|
DeployBoard,
|
||||||
|
Intake,
|
||||||
IssueUserProperty,
|
IssueUserProperty,
|
||||||
Project,
|
Project,
|
||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
|
ProjectNetwork,
|
||||||
State,
|
State,
|
||||||
DEFAULT_STATES,
|
DEFAULT_STATES,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
)
|
)
|
||||||
from plane.utils.cache import cache_response
|
from plane.utils.cache import cache_response
|
||||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
|
||||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
from plane.utils.host import base_host
|
from plane.utils.host import base_host
|
||||||
|
|
||||||
|
|
@ -210,19 +210,25 @@ class ProjectViewSet(BaseViewSet):
|
||||||
|
|
||||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||||
def retrieve(self, request, slug, pk):
|
def retrieve(self, request, slug, pk):
|
||||||
project = (
|
project = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk).first()
|
||||||
self.get_queryset()
|
|
||||||
.filter(
|
|
||||||
project_projectmember__member=self.request.user,
|
|
||||||
project_projectmember__is_active=True,
|
|
||||||
)
|
|
||||||
.filter(archived_at__isnull=True)
|
|
||||||
.filter(pk=pk)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if project is None:
|
if project is None:
|
||||||
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
member_ids = [str(project_member.member_id) for project_member in project.members_list]
|
||||||
|
|
||||||
|
if str(request.user.id) not in member_ids:
|
||||||
|
if project.network == ProjectNetwork.SECRET.value:
|
||||||
|
return Response(
|
||||||
|
{"error": "You do not have permission"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"error": "You are not a member of this project"},
|
||||||
|
status=status.HTTP_409_CONFLICT,
|
||||||
|
)
|
||||||
|
|
||||||
recent_visited_task.delay(
|
recent_visited_task.delay(
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=pk,
|
project_id=pk,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ from .project import (
|
||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
ProjectMemberInvite,
|
ProjectMemberInvite,
|
||||||
|
ProjectNetwork,
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
)
|
)
|
||||||
from .session import Session
|
from .session import Session
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ function IssueDetailsPage({ params }: Route.ComponentProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
{error ? (
|
{error && !issueLoader ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight}
|
image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight}
|
||||||
title={t("issue.empty_state.issue_detail.title")}
|
title={t("issue.empty_state.issue_detail.title")}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-butt
|
||||||
// hooks
|
// hooks
|
||||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||||
import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences";
|
import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences";
|
||||||
|
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
|
||||||
// local imports
|
// local imports
|
||||||
import type { Route } from "./+types/layout";
|
import type { Route } from "./+types/layout";
|
||||||
|
|
||||||
|
|
@ -44,7 +45,9 @@ function ProjectLayout({ params }: Route.ComponentProps) {
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</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 { observer } from "mobx-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { PageHead } from "@/components/core/page-title";
|
import { PageHead } from "@/components/core/page-title";
|
||||||
import { ProfileForm } from "@/components/profile/form";
|
import { ProfileForm } from "@/components/profile/form";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -12,13 +11,7 @@ function ProfileSettingsPage() {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { data: currentUser, userProfile } = useUser();
|
const { data: currentUser, userProfile } = useUser();
|
||||||
|
|
||||||
if (!currentUser)
|
if (!currentUser) return <></>;
|
||||||
return (
|
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={`${t("profile.label")} - ${t("general_settings")}`} />
|
<PageHead title={`${t("profile.label")} - ${t("general_settings")}`} />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { observer } from "mobx-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { PageHead } from "@/components/core/page-title";
|
import { PageHead } from "@/components/core/page-title";
|
||||||
import { PreferencesList } from "@/components/preferences/list";
|
import { PreferencesList } from "@/components/preferences/list";
|
||||||
import { LanguageTimezone } from "@/components/profile/preferences/language-timezone";
|
import { LanguageTimezone } from "@/components/profile/preferences/language-timezone";
|
||||||
|
|
@ -16,30 +15,23 @@ function ProfileAppearancePage() {
|
||||||
// hooks
|
// hooks
|
||||||
const { data: userProfile } = useUserProfile();
|
const { data: userProfile } = useUserProfile();
|
||||||
|
|
||||||
|
if (!userProfile) return <></>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={`${t("profile.label")} - ${t("preferences")}`} />
|
<PageHead title={`${t("profile.label")} - ${t("preferences")}`} />
|
||||||
{userProfile ? (
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<>
|
<div>
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<SettingsHeading
|
||||||
<div>
|
title={t("account_settings.preferences.heading")}
|
||||||
<SettingsHeading
|
description={t("account_settings.preferences.description")}
|
||||||
title={t("account_settings.preferences.heading")}
|
/>
|
||||||
description={t("account_settings.preferences.description")}
|
<PreferencesList />
|
||||||
/>
|
|
||||||
<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>
|
</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 { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
// components
|
// components
|
||||||
|
|
@ -24,12 +23,8 @@ function ProjectSettingsPage({ params }: Route.ComponentProps) {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, projectId } = params;
|
const { workspaceSlug, projectId } = params;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentProjectDetails, fetchProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { allowPermissions } = useUserPermissions();
|
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
|
// derived values
|
||||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId);
|
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"}`}>
|
<div className={`w-full ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
{currentProjectDetails && !isLoading ? (
|
{currentProjectDetails ? (
|
||||||
<ProjectDetailsForm
|
<ProjectDetailsForm
|
||||||
project={currentProjectDetails}
|
project={currentProjectDetails}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
// components
|
// hooks
|
||||||
import { getProjectActivePath } from "@/components/settings/helper";
|
|
||||||
import { SettingsMobileNav } from "@/components/settings/mobile";
|
|
||||||
import { ProjectSettingsSidebar } from "@/components/settings/project/sidebar";
|
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
|
// types
|
||||||
import type { Route } from "./+types/layout";
|
import type { Route } from "./+types/layout";
|
||||||
|
|
||||||
function ProjectSettingsLayout({ params }: Route.ComponentProps) {
|
function ProjectSettingsLayout({ params }: Route.ComponentProps) {
|
||||||
|
const { workspaceSlug, projectId } = params;
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const pathname = usePathname();
|
// store hooks
|
||||||
const { workspaceSlug, projectId } = params;
|
|
||||||
const { joinedProjectIds } = useProject();
|
const { joinedProjectIds } = useProject();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -25,19 +21,7 @@ function ProjectSettingsLayout({ params }: Route.ComponentProps) {
|
||||||
}
|
}
|
||||||
}, [joinedProjectIds, router, workspaceSlug, projectId]);
|
}, [joinedProjectIds, router, workspaceSlug, projectId]);
|
||||||
|
|
||||||
return (
|
return <Outlet />;
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(ProjectSettingsLayout);
|
export default observer(ProjectSettingsLayout);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
// plane imports
|
||||||
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||||
import { cn } from "@plane/utils";
|
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";
|
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||||
|
|
||||||
function ProjectSettingsPage() {
|
function ProjectSettingsPage() {
|
||||||
|
|
@ -10,13 +16,10 @@ function ProjectSettingsPage() {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
const { toggleCreateProjectModal } = useCommandPalette();
|
||||||
// derived values
|
// derived values
|
||||||
const resolvedPath =
|
const resolvedPath = resolvedTheme === "dark" ? ProjectDarkEmptyState : ProjectLightEmptyState;
|
||||||
resolvedTheme === "dark"
|
|
||||||
? "/empty-state/project-settings/no-projects-dark.png"
|
|
||||||
: "/empty-state/project-settings/no-projects-light.png";
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 items-center justify-center h-full max-w-[480px] mx-auto">
|
<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-lg font-semibold text-custom-text-350">No projects yet</div>
|
||||||
<div className="text-sm text-custom-text-350 text-center">
|
<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
|
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[] = [
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// ====================================================================
|
// Archived Projects
|
||||||
// PROJECT LEVEL ROUTES
|
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx", [
|
||||||
// ====================================================================
|
route(
|
||||||
|
":workspaceSlug/projects/archives",
|
||||||
|
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx"
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
// PROJECT LEVEL ROUTES
|
// PROJECT LEVEL ROUTES
|
||||||
|
|
@ -122,136 +126,123 @@ export const coreRoutes: RouteConfigEntry[] = [
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Project Detail
|
// Project Detail
|
||||||
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx", [
|
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [
|
||||||
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [
|
// Project Issues List
|
||||||
// Project Issues List
|
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx", [
|
||||||
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
|
|
||||||
route(
|
route(
|
||||||
":workspaceSlug/projects/:projectId/issues/:issueId",
|
":workspaceSlug/projects/:projectId/issues",
|
||||||
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx"
|
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/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"
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
// Issue Detail
|
||||||
// Project Archives - Issues, Cycles, Modules
|
route(
|
||||||
// Project Archives - Issues - List
|
":workspaceSlug/projects/:projectId/issues/:issueId",
|
||||||
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx", [
|
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.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
|
// Cycle Detail
|
||||||
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx", [
|
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx", [
|
||||||
route(
|
route(
|
||||||
":workspaceSlug/projects/:projectId/archives/cycles",
|
":workspaceSlug/projects/:projectId/cycles/:cycleId",
|
||||||
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx"
|
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx"
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Project Archives - Modules
|
// Cycles List
|
||||||
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx", [
|
layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx", [
|
||||||
route(
|
route(
|
||||||
":workspaceSlug/projects/:projectId/archives/modules",
|
":workspaceSlug/projects/:projectId/cycles",
|
||||||
"./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx"
|
"./(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", [
|
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx", [
|
||||||
// CORE Routes
|
// No Projects available page
|
||||||
// Project Settings
|
|
||||||
route(":workspaceSlug/settings/projects", "./(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx"),
|
route(":workspaceSlug/settings/projects", "./(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx"),
|
||||||
route(
|
layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx", [
|
||||||
":workspaceSlug/settings/projects/:projectId",
|
// Project Settings
|
||||||
"./(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(
|
route(
|
||||||
":workspaceSlug/settings/projects/:projectId/automations",
|
":workspaceSlug/settings/projects/:projectId",
|
||||||
"./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx"
|
"./(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";
|
import { observer } from "mobx-react";
|
||||||
// layouts
|
// layouts
|
||||||
import { ProjectAuthWrapper as CoreProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
import { ProjectAuthWrapper as CoreProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
||||||
|
|
||||||
export type IProjectAuthWrapper = {
|
export type IProjectAuthWrapper = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId?: string;
|
projectId: string;
|
||||||
children: React.ReactNode;
|
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 { ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||||
import { EIssuesStoreType } from "@plane/types";
|
import { EIssuesStoreType } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||||
// hooks
|
// hooks
|
||||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||||
|
|
@ -26,7 +25,7 @@ export const ArchivedIssueLayoutRoot = observer(function ArchivedIssueLayoutRoot
|
||||||
// derived values
|
// derived values
|
||||||
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
|
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
|
||||||
|
|
||||||
const { isLoading } = useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId) {
|
if (workspaceSlug && projectId) {
|
||||||
|
|
@ -36,15 +35,7 @@ export const ArchivedIssueLayoutRoot = observer(function ArchivedIssueLayoutRoot
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return <></>;
|
if (!workspaceSlug || !projectId || !workItemFilters) return <></>;
|
||||||
|
|
||||||
if (isLoading && !workItemFilters)
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssuesStoreContext.Provider value={EIssuesStoreType.ARCHIVED}>
|
<IssuesStoreContext.Provider value={EIssuesStoreType.ARCHIVED}>
|
||||||
<ProjectLevelWorkItemFiltersHOC
|
<ProjectLevelWorkItemFiltersHOC
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import useSWR from "swr";
|
||||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { TransferIssues } from "@/components/cycles/transfer-issues";
|
import { TransferIssues } from "@/components/cycles/transfer-issues";
|
||||||
import { TransferIssuesModal } from "@/components/cycles/transfer-issues-modal";
|
import { TransferIssuesModal } from "@/components/cycles/transfer-issues-modal";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -59,7 +58,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() {
|
||||||
const workItemFilters = cycleId ? issuesFilter?.getIssueFilters(cycleId) : undefined;
|
const workItemFilters = cycleId ? issuesFilter?.getIssueFilters(cycleId) : undefined;
|
||||||
const activeLayout = workItemFilters?.displayFilters?.layout;
|
const activeLayout = workItemFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
const { isLoading } = useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_${workspaceSlug}_${projectId}_${cycleId}` : null,
|
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_${workspaceSlug}_${projectId}_${cycleId}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId && cycleId) {
|
if (workspaceSlug && projectId && cycleId) {
|
||||||
|
|
@ -78,15 +77,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() {
|
||||||
: 0;
|
: 0;
|
||||||
const canTransferIssues = isProgressSnapshotEmpty && transferableIssuesCount > 0;
|
const canTransferIssues = isProgressSnapshotEmpty && transferableIssuesCount > 0;
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return <></>;
|
if (!workspaceSlug || !projectId || !cycleId || !workItemFilters) return <></>;
|
||||||
|
|
||||||
if (isLoading && !workItemFilters)
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssuesStoreContext.Provider value={EIssuesStoreType.CYCLE}>
|
<IssuesStoreContext.Provider value={EIssuesStoreType.CYCLE}>
|
||||||
<ProjectLevelWorkItemFiltersHOC
|
<ProjectLevelWorkItemFiltersHOC
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import useSWR from "swr";
|
||||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||||
import { Row, ERowVariant } from "@plane/ui";
|
import { Row, ERowVariant } from "@plane/ui";
|
||||||
// components
|
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
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 workItemFilters = moduleId ? issuesFilter?.getIssueFilters(moduleId) : undefined;
|
||||||
const activeLayout = workItemFilters?.displayFilters?.layout || undefined;
|
const activeLayout = workItemFilters?.displayFilters?.layout || undefined;
|
||||||
|
|
||||||
const { isLoading } = useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && moduleId
|
workspaceSlug && projectId && moduleId
|
||||||
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
|
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -62,15 +60,7 @@ export const ModuleLayoutRoot = observer(function ModuleLayoutRoot() {
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
if (!workspaceSlug || !projectId || !moduleId || !workItemFilters) return <></>;
|
||||||
|
|
||||||
if (isLoading && !workItemFilters)
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssuesStoreContext.Provider value={EIssuesStoreType.MODULE}>
|
<IssuesStoreContext.Provider value={EIssuesStoreType.MODULE}>
|
||||||
<ProjectLevelWorkItemFiltersHOC
|
<ProjectLevelWorkItemFiltersHOC
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { FC } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import useSWR from "swr";
|
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 { EIssueLayoutTypes, EIssuesStoreType } from "@plane/types";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -49,7 +47,7 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() {
|
||||||
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
|
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
|
||||||
const activeLayout = workItemFilters?.displayFilters?.layout;
|
const activeLayout = workItemFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
const { isLoading } = useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId) {
|
if (workspaceSlug && projectId) {
|
||||||
|
|
@ -59,15 +57,7 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() {
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return <></>;
|
if (!workspaceSlug || !projectId || !workItemFilters) return <></>;
|
||||||
|
|
||||||
if (isLoading && !workItemFilters)
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT}>
|
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT}>
|
||||||
<ProjectLevelWorkItemFiltersHOC
|
<ProjectLevelWorkItemFiltersHOC
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import useSWR from "swr";
|
||||||
// plane constants
|
// plane constants
|
||||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||||
// components
|
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||||
|
|
@ -60,7 +58,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() {
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const { isLoading } = useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null,
|
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId && viewId) {
|
if (workspaceSlug && projectId && viewId) {
|
||||||
|
|
@ -78,16 +76,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() {
|
||||||
[issuesFilter, workspaceSlug, viewId]
|
[issuesFilter, workspaceSlug, viewId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
if (!workspaceSlug || !projectId || !viewId || !workItemFilters) return <></>;
|
||||||
|
|
||||||
if (isLoading && !workItemFilters) {
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-screen w-full items-center justify-center">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT_VIEW}>
|
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT_VIEW}>
|
||||||
<ProjectLevelWorkItemFiltersHOC
|
<ProjectLevelWorkItemFiltersHOC
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { SPREADSHEET_SELECT_GROUP, SPREADSHEET_PROPERTY_LIST } from "@plane/cons
|
||||||
import type { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
import type { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||||
import { EIssueLayoutTypes } from "@plane/types";
|
import { EIssueLayoutTypes } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { MultipleSelectGroup } from "@/components/core/multiple-select";
|
import { MultipleSelectGroup } from "@/components/core/multiple-select";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
|
|
@ -72,13 +71,7 @@ export const SpreadsheetView = observer(function SpreadsheetView(props: Props) {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!issueIds || issueIds.length === 0)
|
if (!issueIds || issueIds.length === 0) return <></>;
|
||||||
return (
|
|
||||||
<div className="grid h-full w-full place-items-center">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 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" />
|
<div ref={portalRef} className="spreadsheet-menu-portal" />
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import type { IWorkspace } from "@plane/types";
|
||||||
import { CustomSelect, Input } from "@plane/ui";
|
import { CustomSelect, Input } from "@plane/ui";
|
||||||
import { copyUrlToClipboard, getFileURL } from "@plane/utils";
|
import { copyUrlToClipboard, getFileURL } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal";
|
import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal";
|
||||||
// helpers
|
// helpers
|
||||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
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);
|
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||||
|
|
||||||
if (!currentWorkspace)
|
if (!currentWorkspace) return <></>;
|
||||||
return (
|
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import type { FC, ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { GANTT_TIMELINE_TYPE } from "@plane/types";
|
||||||
import { EmptyStateDetailed } from "@plane/propel/empty-state";
|
|
||||||
import { EProjectNetwork, GANTT_TIMELINE_TYPE } from "@plane/types";
|
|
||||||
// components
|
// components
|
||||||
import { JoinProject } from "@/components/auth-screens/project/join-project";
|
import { ProjectAccessRestriction } from "@/components/auth-screens/project/project-access-restriction";
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import {
|
import {
|
||||||
PROJECT_DETAILS,
|
PROJECT_DETAILS,
|
||||||
PROJECT_ME_INFORMATION,
|
PROJECT_ME_INFORMATION,
|
||||||
|
|
@ -23,10 +20,8 @@ import {
|
||||||
PROJECT_VIEWS,
|
PROJECT_VIEWS,
|
||||||
PROJECT_INTAKE_STATE,
|
PROJECT_INTAKE_STATE,
|
||||||
} from "@/constants/fetch-keys";
|
} from "@/constants/fetch-keys";
|
||||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectEstimates } from "@/hooks/store/estimates";
|
import { useProjectEstimates } from "@/hooks/store/estimates";
|
||||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
|
||||||
import { useCycle } from "@/hooks/store/use-cycle";
|
import { useCycle } from "@/hooks/store/use-cycle";
|
||||||
import { useLabel } from "@/hooks/store/use-label";
|
import { useLabel } from "@/hooks/store/use-label";
|
||||||
import { useMember } from "@/hooks/store/use-member";
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
|
|
@ -39,19 +34,19 @@ import { useTimeLineChart } from "@/hooks/use-timeline-chart";
|
||||||
|
|
||||||
interface IProjectAuthWrapper {
|
interface IProjectAuthWrapper {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId?: string;
|
projectId: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IProjectAuthWrapper) {
|
export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IProjectAuthWrapper) {
|
||||||
const { workspaceSlug, projectId, children, isLoading: isParentLoading = false } = props;
|
const { workspaceSlug, projectId, children, isLoading: isParentLoading = false } = props;
|
||||||
// plane hooks
|
// states
|
||||||
const { t } = useTranslation();
|
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
const { fetchUserProjectInfo, allowPermissions } = useUserPermissions();
|
||||||
const { fetchUserProjectInfo, allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
const { fetchProjectDetails } = useProject();
|
||||||
const { loader, getProjectById, fetchProjectDetails } = useProject();
|
const { joinProject } = useUserPermissions();
|
||||||
const { fetchAllCycles } = useCycle();
|
const { fetchAllCycles } = useCycle();
|
||||||
const { fetchModulesSlim, fetchModules } = useModule();
|
const { fetchModulesSlim, fetchModules } = useModule();
|
||||||
const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE);
|
const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE);
|
||||||
|
|
@ -63,10 +58,7 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
|
||||||
const { data: currentUserData } = useUser();
|
const { data: currentUserData } = useUser();
|
||||||
const { fetchProjectLabels } = useLabel();
|
const { fetchProjectLabels } = useLabel();
|
||||||
const { getProjectEstimates } = useProjectEstimates();
|
const { getProjectEstimates } = useProjectEstimates();
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const projectExists = projectId ? getProjectById(projectId) : null;
|
|
||||||
const projectMemberInfo = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
|
||||||
const hasPermissionToCurrentProject = allowPermissions(
|
const hasPermissionToCurrentProject = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||||
EUserPermissionsLevel.PROJECT,
|
EUserPermissionsLevel.PROJECT,
|
||||||
|
|
@ -82,120 +74,84 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// fetching project details
|
// fetching project details
|
||||||
useSWR(
|
const { isLoading: isProjectDetailsLoading, error: projectDetailsError } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(workspaceSlug, projectId) : null,
|
PROJECT_DETAILS(workspaceSlug, projectId),
|
||||||
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug, projectId) : null
|
() => fetchProjectDetails(workspaceSlug, projectId)
|
||||||
);
|
);
|
||||||
|
// fetching user project member information
|
||||||
|
useSWR(PROJECT_ME_INFORMATION(workspaceSlug, projectId), () => fetchUserProjectInfo(workspaceSlug, projectId));
|
||||||
// fetching project member preferences
|
// fetching project member preferences
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(workspaceSlug, projectId) : null,
|
currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(workspaceSlug, projectId) : null,
|
||||||
workspaceSlug && projectId && currentUserData?.id
|
currentUserData?.id ? () => fetchProjectMemberPreferences(workspaceSlug, projectId, currentUserData.id) : null,
|
||||||
? () => fetchProjectMemberPreferences(workspaceSlug, projectId, currentUserData.id)
|
|
||||||
: null,
|
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetching project labels
|
// fetching project labels
|
||||||
useSWR(
|
useSWR(PROJECT_LABELS(workspaceSlug, projectId), () => fetchProjectLabels(workspaceSlug, projectId), {
|
||||||
workspaceSlug && projectId ? PROJECT_LABELS(workspaceSlug, projectId) : null,
|
revalidateIfStale: false,
|
||||||
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug, projectId) : null,
|
revalidateOnFocus: false,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
});
|
||||||
);
|
|
||||||
// fetching project members
|
// fetching project members
|
||||||
useSWR(
|
useSWR(PROJECT_MEMBERS(workspaceSlug, projectId), () => fetchProjectMembers(workspaceSlug, projectId), {
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug, projectId) : null,
|
revalidateIfStale: false,
|
||||||
workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug, projectId) : null,
|
revalidateOnFocus: false,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
});
|
||||||
);
|
|
||||||
// fetching project states
|
// fetching project states
|
||||||
useSWR(
|
useSWR(PROJECT_STATES(workspaceSlug, projectId), () => fetchProjectStates(workspaceSlug, projectId), {
|
||||||
workspaceSlug && projectId ? PROJECT_STATES(workspaceSlug, projectId) : null,
|
revalidateIfStale: false,
|
||||||
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug, projectId) : null,
|
revalidateOnFocus: false,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
});
|
||||||
);
|
|
||||||
// fetching project intake state
|
// fetching project intake state
|
||||||
useSWR(
|
useSWR(PROJECT_INTAKE_STATE(workspaceSlug, projectId), () => fetchProjectIntakeState(workspaceSlug, projectId), {
|
||||||
workspaceSlug && projectId ? PROJECT_INTAKE_STATE(workspaceSlug, projectId) : null,
|
revalidateIfStale: false,
|
||||||
workspaceSlug && projectId ? () => fetchProjectIntakeState(workspaceSlug, projectId) : null,
|
revalidateOnFocus: false,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
});
|
||||||
);
|
|
||||||
// fetching project estimates
|
// fetching project estimates
|
||||||
useSWR(
|
useSWR(PROJECT_ESTIMATES(workspaceSlug, projectId), () => getProjectEstimates(workspaceSlug, projectId), {
|
||||||
workspaceSlug && projectId ? PROJECT_ESTIMATES(workspaceSlug, projectId) : null,
|
revalidateIfStale: false,
|
||||||
workspaceSlug && projectId ? () => getProjectEstimates(workspaceSlug, projectId) : null,
|
revalidateOnFocus: false,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
});
|
||||||
);
|
|
||||||
// fetching project cycles
|
// fetching project cycles
|
||||||
useSWR(
|
useSWR(PROJECT_ALL_CYCLES(workspaceSlug, projectId), () => fetchAllCycles(workspaceSlug, projectId), {
|
||||||
workspaceSlug && projectId ? PROJECT_ALL_CYCLES(workspaceSlug, projectId) : null,
|
revalidateIfStale: false,
|
||||||
workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug, projectId) : null,
|
revalidateOnFocus: false,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
});
|
||||||
);
|
|
||||||
// fetching project modules
|
// fetching project modules
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_MODULES(workspaceSlug, projectId) : null,
|
PROJECT_MODULES(workspaceSlug, projectId),
|
||||||
workspaceSlug && projectId
|
async () => {
|
||||||
? async () => {
|
await Promise.all([fetchModulesSlim(workspaceSlug, projectId), fetchModules(workspaceSlug, projectId)]);
|
||||||
await fetchModulesSlim(workspaceSlug, projectId);
|
},
|
||||||
await fetchModules(workspaceSlug, projectId);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetching project views
|
// fetching project views
|
||||||
useSWR(
|
useSWR(PROJECT_VIEWS(workspaceSlug, projectId), () => fetchViews(workspaceSlug, projectId), {
|
||||||
workspaceSlug && projectId ? PROJECT_VIEWS(workspaceSlug, projectId) : null,
|
revalidateIfStale: false,
|
||||||
workspaceSlug && projectId ? () => fetchViews(workspaceSlug, projectId) : null,
|
revalidateOnFocus: false,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// permissions
|
// handle join project
|
||||||
const canPerformEmptyStateActions = allowPermissions(
|
const handleJoinProject = () => {
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
setIsJoiningProject(true);
|
||||||
EUserPermissionsLevel.WORKSPACE
|
joinProject(workspaceSlug, projectId)
|
||||||
);
|
.then(() => fetchProjectDetails(workspaceSlug, projectId))
|
||||||
|
.finally(() => setIsJoiningProject(false));
|
||||||
|
};
|
||||||
|
|
||||||
// check if the project member apis is loading
|
const isProjectLoading = (isParentLoading || isProjectDetailsLoading) && !projectDetailsError;
|
||||||
if (isParentLoading || (!projectMemberInfo && projectId && hasPermissionToCurrentProject === null))
|
|
||||||
|
if (isProjectLoading) return null;
|
||||||
|
|
||||||
|
if (!isProjectLoading && hasPermissionToCurrentProject === false) {
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full place-items-center bg-custom-background-100 p-4 rounded-lg border border-custom-border-200">
|
<ProjectAccessRestriction
|
||||||
<div className="flex flex-col items-center gap-3 text-center">
|
errorStatusCode={projectDetailsError?.status}
|
||||||
<LogoSpinner />
|
isWorkspaceAdmin={isWorkspaceAdmin}
|
||||||
</div>
|
handleJoinProject={handleJoinProject}
|
||||||
</div>
|
isJoinButtonDisabled={isJoiningProject}
|
||||||
);
|
/>
|
||||||
|
|
||||||
// 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>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export class ProjectService extends APIService {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Začněte s vaší první pracovní položkou.",
|
title: "Začněte s vaší první pracovní položkou.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Beginnen Sie mit Ihrem ersten Arbeitselement.",
|
title: "Beginnen Sie mit Ihrem ersten Arbeitselement.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "Seems like you don’t 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: {
|
work_items: {
|
||||||
title: "Start with your first work item.",
|
title: "Start with your first work item.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Comienza con tu primer elemento de trabajo.",
|
title: "Comienza con tu primer elemento de trabajo.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "Il semble que vous n’ayez pas accès à ce projet",
|
||||||
|
restricted_description: "Contactez l’administrateur pour demander l’accè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 n’existe pas.",
|
||||||
|
},
|
||||||
work_items: {
|
work_items: {
|
||||||
title: "Commencez avec votre premier élément de travail.",
|
title: "Commencez avec votre premier élément de travail.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Mulai dengan item kerja pertama Anda.",
|
title: "Mulai dengan item kerja pertama Anda.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Inizia con il tuo primo elemento di lavoro.",
|
title: "Inizia con il tuo primo elemento di lavoro.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "このプロジェクトへのアクセス権がないようです",
|
||||||
|
restricted_description: "管理者に連絡してアクセス権をリクエストすると、ここで作業を続けられます。",
|
||||||
|
join_description: "下のボタンをクリックして参加してください。",
|
||||||
|
cta_primary: "プロジェクトに参加",
|
||||||
|
cta_loading: "プロジェクトに参加中",
|
||||||
|
},
|
||||||
|
invalid_project: {
|
||||||
|
title: "プロジェクトが見つかりません",
|
||||||
|
description: "お探しのプロジェクトは存在しません。",
|
||||||
|
},
|
||||||
work_items: {
|
work_items: {
|
||||||
title: "最初の作業項目から始めましょう。",
|
title: "最初の作業項目から始めましょう。",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "이 프로젝트에 접근할 수 없는 것 같습니다",
|
||||||
|
restricted_description: "관리자에게 접근 권한을 요청하시면 여기서 계속 진행하실 수 있습니다.",
|
||||||
|
join_description: "아래 버튼을 클릭하여 프로젝트에 참여하세요.",
|
||||||
|
cta_primary: "프로젝트 참여",
|
||||||
|
cta_loading: "프로젝트 참여 중",
|
||||||
|
},
|
||||||
|
invalid_project: {
|
||||||
|
title: "프로젝트를 찾을 수 없습니다",
|
||||||
|
description: "찾으시는 프로젝트가 존재하지 않습니다.",
|
||||||
|
},
|
||||||
work_items: {
|
work_items: {
|
||||||
title: "첫 번째 작업 항목으로 시작하세요.",
|
title: "첫 번째 작업 항목으로 시작하세요.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Zacznij od swojego pierwszego elementu roboczego.",
|
title: "Zacznij od swojego pierwszego elementu roboczego.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Comece com seu primeiro item de trabalho.",
|
title: "Comece com seu primeiro item de trabalho.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Începeți cu primul dvs. element de lucru.",
|
title: "Începeți cu primul dvs. element de lucru.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "Похоже, у вас нет доступа к этому проекту",
|
||||||
|
restricted_description: "Свяжитесь с администратором, чтобы запросить доступ, и вы сможете продолжить здесь.",
|
||||||
|
join_description: "Нажмите кнопку ниже, чтобы присоединиться.",
|
||||||
|
cta_primary: "Присоединиться к проекту",
|
||||||
|
cta_loading: "Присоединение к проекту",
|
||||||
|
},
|
||||||
|
invalid_project: {
|
||||||
|
title: "Проект не найден",
|
||||||
|
description: "Проект, который вы ищете, не существует.",
|
||||||
|
},
|
||||||
work_items: {
|
work_items: {
|
||||||
title: "Начните с вашего первого рабочего элемента.",
|
title: "Начните с вашего первого рабочего элемента.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Začnite s vašou prvou pracovnou položkou.",
|
title: "Začnite s vašou prvou pracovnou položkou.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "İlk iş öğenizle başlayın.",
|
title: "İlk iş öğenizle başlayın.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "Схоже, у вас немає доступу до цього проєкту",
|
||||||
|
restricted_description: "Зверніться до адміністратора, щоб запросити доступ, і ви зможете продовжити тут.",
|
||||||
|
join_description: "Натисніть кнопку нижче, щоб приєднатися.",
|
||||||
|
cta_primary: "Приєднатися до проєкту",
|
||||||
|
cta_loading: "Приєднання до проєкту",
|
||||||
|
},
|
||||||
|
invalid_project: {
|
||||||
|
title: "Проєкт не знайдено",
|
||||||
|
description: "Проєкт, який ви шукаєте, не існує.",
|
||||||
|
},
|
||||||
work_items: {
|
work_items: {
|
||||||
title: "Почніть з вашого першого робочого елемента.",
|
title: "Почніть з вашого першого робочого елемента.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
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: {
|
work_items: {
|
||||||
title: "Bắt đầu với mục công việc đầu tiên của bạn.",
|
title: "Bắt đầu với mục công việc đầu tiên của bạn.",
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "您似乎无权访问该项目",
|
||||||
|
restricted_description: "请联系管理员申请访问权限,通过后您可以在此继续。",
|
||||||
|
join_description: "点击下方按钮加入该项目。",
|
||||||
|
cta_primary: "加入项目",
|
||||||
|
cta_loading: "正在加入项目",
|
||||||
|
},
|
||||||
|
invalid_project: {
|
||||||
|
title: "未找到项目",
|
||||||
|
description: "您查找的项目不存在。",
|
||||||
|
},
|
||||||
work_items: {
|
work_items: {
|
||||||
title: "从您的第一个工作项开始。",
|
title: "从您的第一个工作项开始。",
|
||||||
description: "工作项是项目的构建块 — 分配负责人、设置优先级并轻松跟踪进度。",
|
description: "工作项是项目的构建块 — 分配负责人、设置优先级并轻松跟踪进度。",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project_empty_state: {
|
project_empty_state: {
|
||||||
|
no_access: {
|
||||||
|
title: "您似乎無權存取此專案",
|
||||||
|
restricted_description: "請聯絡管理員以申請存取權,核准後即可在此繼續。",
|
||||||
|
join_description: "點擊下方按鈕加入專案。",
|
||||||
|
cta_primary: "加入專案",
|
||||||
|
cta_loading: "正在加入專案",
|
||||||
|
},
|
||||||
|
invalid_project: {
|
||||||
|
title: "找不到專案",
|
||||||
|
description: "您所查找的專案不存在。",
|
||||||
|
},
|
||||||
work_items: {
|
work_items: {
|
||||||
title: "從您的第一個工作項開始。",
|
title: "從您的第一個工作項開始。",
|
||||||
description: "工作項是專案的建構模組 — 指派負責人、設定優先順序並輕鬆追蹤進度。",
|
description: "工作項是專案的建構模組 — 指派負責人、設定優先順序並輕鬆追蹤進度。",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue