[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
|
||||
import boto3
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
import json
|
||||
|
||||
import boto3
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.utils import timezone
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.app.permissions import ROLE, ProjectMemberPermission, allow_permission
|
||||
from plane.app.serializers import (
|
||||
ProjectSerializer,
|
||||
ProjectListSerializer,
|
||||
DeployBoardSerializer,
|
||||
ProjectListSerializer,
|
||||
ProjectSerializer,
|
||||
)
|
||||
|
||||
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
|
||||
from plane.app.views.base import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
Intake,
|
||||
DeployBoard,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
ProjectNetwork,
|
||||
State,
|
||||
DEFAULT_STATES,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.utils.cache import cache_response
|
||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
|
@ -210,19 +210,25 @@ class ProjectViewSet(BaseViewSet):
|
|||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def retrieve(self, request, slug, pk):
|
||||
project = (
|
||||
self.get_queryset()
|
||||
.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(archived_at__isnull=True)
|
||||
.filter(pk=pk)
|
||||
).first()
|
||||
project = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk).first()
|
||||
|
||||
if project is None:
|
||||
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
member_ids = [str(project_member.member_id) for project_member in project.members_list]
|
||||
|
||||
if str(request.user.id) not in member_ids:
|
||||
if project.network == ProjectNetwork.SECRET.value:
|
||||
return Response(
|
||||
{"error": "You do not have permission"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "You are not a member of this project"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
project_id=pk,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ from .project import (
|
|||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
ProjectMemberInvite,
|
||||
ProjectNetwork,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
from .session import Session
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Vypadá to, že nemáte přístup k tomuto projektu",
|
||||
restricted_description: "Kontaktujte administrátora a požádejte o přístup, abyste zde mohli pokračovat.",
|
||||
join_description: "Klikněte na tlačítko níže pro připojení k projektu.",
|
||||
cta_primary: "Připojit se k projektu",
|
||||
cta_loading: "Připojování k projektu",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Projekt nebyl nalezen",
|
||||
description: "Projekt, který hledáte, neexistuje.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Začněte s vaší první pracovní položkou.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Es scheint, als hätten Sie keinen Zugriff auf dieses Projekt",
|
||||
restricted_description: "Kontaktieren Sie den Administrator, um Zugriff anzufordern, damit Sie hier fortfahren können.",
|
||||
join_description: "Klicken Sie unten auf die Schaltfläche, um beizutreten.",
|
||||
cta_primary: "Projekt beitreten",
|
||||
cta_loading: "Projekt wird beigetreten",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Projekt nicht gefunden",
|
||||
description: "Das gesuchte Projekt existiert nicht.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Beginnen Sie mit Ihrem ersten Arbeitselement.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Seems like you 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: {
|
||||
title: "Start with your first work item.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Parece que no tienes acceso a este proyecto",
|
||||
restricted_description: "Contacta con el administrador para solicitar acceso y podrás continuar aquí.",
|
||||
join_description: "Haz clic en el botón de abajo para unirte.",
|
||||
cta_primary: "Unirse al proyecto",
|
||||
cta_loading: "Uniéndose al proyecto",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Proyecto no encontrado",
|
||||
description: "El proyecto que buscas no existe.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Comienza con tu primer elemento de trabajo.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Il semble que vous 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: {
|
||||
title: "Commencez avec votre premier élément de travail.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Sepertinya Anda tidak memiliki akses ke Proyek ini",
|
||||
restricted_description: "Hubungi admin untuk meminta akses agar Anda dapat melanjutkan di sini.",
|
||||
join_description: "Klik tombol di bawah ini untuk bergabung.",
|
||||
cta_primary: "Bergabung dengan proyek",
|
||||
cta_loading: "Sedang bergabung dengan proyek",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Proyek tidak ditemukan",
|
||||
description: "Proyek yang Anda cari tidak ada.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Mulai dengan item kerja pertama Anda.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Sembra che tu non abbia accesso a questo progetto",
|
||||
restricted_description: "Contatta l'amministratore per richiedere l'accesso e potrai continuare qui.",
|
||||
join_description: "Clicca sul pulsante qui sotto per unirti.",
|
||||
cta_primary: "Unisciti al progetto",
|
||||
cta_loading: "Unione al progetto in corso",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Progetto non trovato",
|
||||
description: "Il progetto che stai cercando non esiste.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Inizia con il tuo primo elemento di lavoro.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "このプロジェクトへのアクセス権がないようです",
|
||||
restricted_description: "管理者に連絡してアクセス権をリクエストすると、ここで作業を続けられます。",
|
||||
join_description: "下のボタンをクリックして参加してください。",
|
||||
cta_primary: "プロジェクトに参加",
|
||||
cta_loading: "プロジェクトに参加中",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "プロジェクトが見つかりません",
|
||||
description: "お探しのプロジェクトは存在しません。",
|
||||
},
|
||||
work_items: {
|
||||
title: "最初の作業項目から始めましょう。",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "이 프로젝트에 접근할 수 없는 것 같습니다",
|
||||
restricted_description: "관리자에게 접근 권한을 요청하시면 여기서 계속 진행하실 수 있습니다.",
|
||||
join_description: "아래 버튼을 클릭하여 프로젝트에 참여하세요.",
|
||||
cta_primary: "프로젝트 참여",
|
||||
cta_loading: "프로젝트 참여 중",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "프로젝트를 찾을 수 없습니다",
|
||||
description: "찾으시는 프로젝트가 존재하지 않습니다.",
|
||||
},
|
||||
work_items: {
|
||||
title: "첫 번째 작업 항목으로 시작하세요.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Wygląda na to, że nie masz dostępu do tego projektu",
|
||||
restricted_description: "Skontaktuj się z administratorem, aby poprosić o dostęp i móc kontynuować tutaj.",
|
||||
join_description: "Kliknij przycisk poniżej, aby dołączyć.",
|
||||
cta_primary: "Dołącz do projektu",
|
||||
cta_loading: "Dołączanie do projektu",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Projekt nie został znaleziony",
|
||||
description: "Projekt, którego szukasz, nie istnieje.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Zacznij od swojego pierwszego elementu roboczego.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Parece que você não tem acesso a este projeto",
|
||||
restricted_description: "Entre em contato com o administrador para solicitar acesso e você poderá continuar aqui.",
|
||||
join_description: "Clique no botão abaixo para participar.",
|
||||
cta_primary: "Participar do projeto",
|
||||
cta_loading: "Participando do projeto",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Projeto não encontrado",
|
||||
description: "O projeto que você está procurando não existe.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Comece com seu primeiro item de trabalho.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Se pare că nu aveți acces la acest proiect",
|
||||
restricted_description: "Contactați administratorul pentru a solicita accesul și veți putea continua aici.",
|
||||
join_description: "Faceți clic pe butonul de mai jos pentru a vă alătura.",
|
||||
cta_primary: "Alăturați-vă proiectului",
|
||||
cta_loading: "Se alătură proiectului",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Proiect negăsit",
|
||||
description: "Proiectul pe care îl căutați nu există.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Începeți cu primul dvs. element de lucru.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Похоже, у вас нет доступа к этому проекту",
|
||||
restricted_description: "Свяжитесь с администратором, чтобы запросить доступ, и вы сможете продолжить здесь.",
|
||||
join_description: "Нажмите кнопку ниже, чтобы присоединиться.",
|
||||
cta_primary: "Присоединиться к проекту",
|
||||
cta_loading: "Присоединение к проекту",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Проект не найден",
|
||||
description: "Проект, который вы ищете, не существует.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Начните с вашего первого рабочего элемента.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Zdá sa, že nemáte prístup k tomuto projektu",
|
||||
restricted_description: "Kontaktujte administrátora, aby ste požiadali o prístup, a potom tu môžete pokračovať.",
|
||||
join_description: "Kliknite na tlačidlo nižšie, aby ste sa pripojili.",
|
||||
cta_primary: "Pripojiť sa k projektu",
|
||||
cta_loading: "Pripájanie k projektu",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Projekt nebol nájdený",
|
||||
description: "Projekt, ktorý hľadáte, neexistuje.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Začnite s vašou prvou pracovnou položkou.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Görünüşe göre bu projeye erişiminiz yok",
|
||||
restricted_description: "Erişim talep etmek için yöneticiyle iletişime geçin, sonra burada devam edebilirsiniz.",
|
||||
join_description: "Katılmak için aşağıdaki butona tıklayın.",
|
||||
cta_primary: "Projeye katıl",
|
||||
cta_loading: "Projeye katılınıyor",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Proje bulunamadı",
|
||||
description: "Aradığınız proje mevcut değil.",
|
||||
},
|
||||
work_items: {
|
||||
title: "İlk iş öğenizle başlayın.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Схоже, у вас немає доступу до цього проєкту",
|
||||
restricted_description: "Зверніться до адміністратора, щоб запросити доступ, і ви зможете продовжити тут.",
|
||||
join_description: "Натисніть кнопку нижче, щоб приєднатися.",
|
||||
cta_primary: "Приєднатися до проєкту",
|
||||
cta_loading: "Приєднання до проєкту",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Проєкт не знайдено",
|
||||
description: "Проєкт, який ви шукаєте, не існує.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Почніть з вашого першого робочого елемента.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "Có vẻ như bạn không có quyền truy cập vào Dự án này",
|
||||
restricted_description: "Liên hệ với quản trị viên để yêu cầu quyền truy cập và bạn có thể tiếp tục tại đây.",
|
||||
join_description: "Nhấn nút bên dưới để tham gia.",
|
||||
cta_primary: "Tham gia dự án",
|
||||
cta_loading: "Đang tham gia dự án",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "Không tìm thấy dự án",
|
||||
description: "Dự án bạn đang tìm kiếm không tồn tại.",
|
||||
},
|
||||
work_items: {
|
||||
title: "Bắt đầu với mục công việc đầu tiên của bạn.",
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "您似乎无权访问该项目",
|
||||
restricted_description: "请联系管理员申请访问权限,通过后您可以在此继续。",
|
||||
join_description: "点击下方按钮加入该项目。",
|
||||
cta_primary: "加入项目",
|
||||
cta_loading: "正在加入项目",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "未找到项目",
|
||||
description: "您查找的项目不存在。",
|
||||
},
|
||||
work_items: {
|
||||
title: "从您的第一个工作项开始。",
|
||||
description: "工作项是项目的构建块 — 分配负责人、设置优先级并轻松跟踪进度。",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export default {
|
|||
},
|
||||
},
|
||||
project_empty_state: {
|
||||
no_access: {
|
||||
title: "您似乎無權存取此專案",
|
||||
restricted_description: "請聯絡管理員以申請存取權,核准後即可在此繼續。",
|
||||
join_description: "點擊下方按鈕加入專案。",
|
||||
cta_primary: "加入專案",
|
||||
cta_loading: "正在加入專案",
|
||||
},
|
||||
invalid_project: {
|
||||
title: "找不到專案",
|
||||
description: "您所查找的專案不存在。",
|
||||
},
|
||||
work_items: {
|
||||
title: "從您的第一個工作項開始。",
|
||||
description: "工作項是專案的建構模組 — 指派負責人、設定優先順序並輕鬆追蹤進度。",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue