feat(tasker): open project settings in modal

This commit is contained in:
DCCONSTRUCTIONS 2026-05-10 19:58:30 +03:00
parent bf75ce84eb
commit e49840341b
16 changed files with 725 additions and 40 deletions

View File

@ -25,13 +25,13 @@ import { CycleAppliedFiltersList } from "@/components/cycles/applied-filters";
import { CyclesView } from "@/components/cycles/cycles-view"; import { CyclesView } from "@/components/cycles/cycles-view";
import { CycleCreateUpdateModal } from "@/components/cycles/modal"; import { CycleCreateUpdateModal } from "@/components/cycles/modal";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader";
// hooks // hooks
import { useCycle } from "@/hooks/store/use-cycle"; import { useCycle } from "@/hooks/store/use-cycle";
import { useCycleFilter } from "@/hooks/store/use-cycle-filter"; import { useCycleFilter } from "@/hooks/store/use-cycle-filter";
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
function ProjectCyclesPage({ params }: Route.ComponentProps) { function ProjectCyclesPage({ params }: Route.ComponentProps) {
@ -40,8 +40,6 @@ function ProjectCyclesPage({ params }: Route.ComponentProps) {
// store hooks // store hooks
const { currentProjectCycleIds, loader } = useCycle(); const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById, currentProjectDetails } = useProject(); const { getProjectById, currentProjectDetails } = useProject();
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = params; const { workspaceSlug, projectId } = params;
// theme hook // theme hook
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
@ -81,7 +79,7 @@ function ProjectCyclesPage({ params }: Route.ComponentProps) {
primaryButton={{ primaryButton={{
text: t("disabled_project.empty_state.cycle.primary_button.text"), text: t("disabled_project.empty_state.cycle.primary_button.text"),
onClick: () => { onClick: () => {
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); openProjectSettingsModal(projectId, "features_cycles");
}, },
disabled: !hasAdminLevelPermission, disabled: !hasAdminLevelPermission,
}} }}

View File

@ -18,15 +18,13 @@ import lightIntakeAsset from "@/app/assets/empty-state/disabled-feature/intake-l
import { PageHead } from "@/components/core/page-title"; import { PageHead } from "@/components/core/page-title";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { InboxIssueRoot } from "@/components/inbox"; import { InboxIssueRoot } from "@/components/inbox";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
// hooks // hooks
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
function ProjectInboxPage({ params }: Route.ComponentProps) { function ProjectInboxPage({ params }: Route.ComponentProps) {
/// router
const router = useAppRouter();
const { workspaceSlug, projectId } = params; const { workspaceSlug, projectId } = params;
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const navigationTab = searchParams.get("currentTab"); const navigationTab = searchParams.get("currentTab");
@ -53,7 +51,7 @@ function ProjectInboxPage({ params }: Route.ComponentProps) {
primaryButton={{ primaryButton={{
text: t("disabled_project.empty_state.inbox.primary_button.text"), text: t("disabled_project.empty_state.inbox.primary_button.text"),
onClick: () => { onClick: () => {
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); openProjectSettingsModal(projectId, "features_intake");
}, },
disabled: !canPerformEmptyStateActions, disabled: !canPerformEmptyStateActions,
}} }}

View File

@ -20,17 +20,15 @@ import lightModulesAsset from "@/app/assets/empty-state/disabled-feature/modules
import { PageHead } from "@/components/core/page-title"; import { PageHead } from "@/components/core/page-title";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules"; import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
// hooks // hooks
import { useModuleFilter } from "@/hooks/store/use-module-filter"; import { useModuleFilter } from "@/hooks/store/use-module-filter";
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
function ProjectModulesPage({ params }: Route.ComponentProps) { function ProjectModulesPage({ params }: Route.ComponentProps) {
// router const { projectId } = params;
const router = useAppRouter();
const { workspaceSlug, projectId } = params;
// theme hook // theme hook
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
// plane hooks // plane hooks
@ -74,7 +72,7 @@ function ProjectModulesPage({ params }: Route.ComponentProps) {
primaryButton={{ primaryButton={{
text: t("disabled_project.empty_state.module.primary_button.text"), text: t("disabled_project.empty_state.module.primary_button.text"),
onClick: () => { onClick: () => {
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); openProjectSettingsModal(projectId, "features_modules");
}, },
disabled: !canPerformEmptyStateActions, disabled: !canPerformEmptyStateActions,
}} }}

View File

@ -20,10 +20,10 @@ import { PageHead } from "@/components/core/page-title";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { PagesListRoot } from "@/components/pages/list/root"; import { PagesListRoot } from "@/components/pages/list/root";
import { PagesListView } from "@/components/pages/pages-list-view"; import { PagesListView } from "@/components/pages/pages-list-view";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
// hooks // hooks
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web hooks // plane web hooks
import { EPageStoreType } from "@/plane-web/hooks/store"; import { EPageStoreType } from "@/plane-web/hooks/store";
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
@ -36,7 +36,6 @@ const getPageType = (pageType?: string | null): TPageNavigationTabs => {
function ProjectPagesPage({ params }: Route.ComponentProps) { function ProjectPagesPage({ params }: Route.ComponentProps) {
// router // router
const router = useAppRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const type = searchParams.get("type"); const type = searchParams.get("type");
const { workspaceSlug, projectId } = params; const { workspaceSlug, projectId } = params;
@ -65,7 +64,7 @@ function ProjectPagesPage({ params }: Route.ComponentProps) {
primaryButton={{ primaryButton={{
text: t("disabled_project.empty_state.page.primary_button.text"), text: t("disabled_project.empty_state.page.primary_button.text"),
onClick: () => { onClick: () => {
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); openProjectSettingsModal(projectId, "features_pages");
}, },
disabled: !canPerformEmptyStateActions, disabled: !canPerformEmptyStateActions,
}} }}

View File

@ -20,19 +20,17 @@ import lightViewsAsset from "@/app/assets/empty-state/disabled-feature/views-lig
// components // components
import { PageHead } from "@/components/core/page-title"; import { PageHead } from "@/components/core/page-title";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
import { ViewAppliedFiltersList } from "@/components/views/applied-filters"; import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
import { ProjectViewsList } from "@/components/views/views-list"; import { ProjectViewsList } from "@/components/views/views-list";
// hooks // hooks
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useProjectView } from "@/hooks/store/use-project-view"; import { useProjectView } from "@/hooks/store/use-project-view";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
function ProjectViewsPage({ params }: Route.ComponentProps) { function ProjectViewsPage({ params }: Route.ComponentProps) {
// router const { projectId } = params;
const router = useAppRouter();
const { workspaceSlug, projectId } = params;
// theme hook // theme hook
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
// plane hooks // plane hooks
@ -77,7 +75,7 @@ function ProjectViewsPage({ params }: Route.ComponentProps) {
primaryButton={{ primaryButton={{
text: t("disabled_project.empty_state.view.primary_button.text"), text: t("disabled_project.empty_state.view.primary_button.text"),
onClick: () => { onClick: () => {
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); openProjectSettingsModal(projectId, "features_views");
}, },
disabled: !canPerformEmptyStateActions, disabled: !canPerformEmptyStateActions,
}} }}

View File

@ -6,8 +6,12 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Outlet } from "react-router"; import { Outlet, redirect } from "react-router";
// components // components
import {
buildProjectSettingsModalUrl,
getProjectSettingsModalTabFromPath,
} from "@/components/project/settings/project-settings-modal.utils";
import { getProjectActivePath } from "@/components/settings/helper"; import { getProjectActivePath } from "@/components/settings/helper";
import { SettingsMobileNav } from "@/components/settings/mobile/nav"; import { SettingsMobileNav } from "@/components/settings/mobile/nav";
// layouts // layouts
@ -16,6 +20,13 @@ import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
import type { Route } from "./+types/layout"; import type { Route } from "./+types/layout";
import { ProjectSettingsSidebarRoot } from "@/components/settings/project/sidebar"; import { ProjectSettingsSidebarRoot } from "@/components/settings/project/sidebar";
export const clientLoader = ({ params, request }: Route.ClientLoaderArgs) => {
const { workspaceSlug, projectId } = params;
const tab = getProjectSettingsModalTabFromPath(new URL(request.url).pathname);
throw redirect(buildProjectSettingsModalUrl(workspaceSlug, projectId, tab));
};
function ProjectDetailSettingsLayout({ params }: Route.ComponentProps) { function ProjectDetailSettingsLayout({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params; const { workspaceSlug, projectId } = params;
// router // router

View File

@ -7,6 +7,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import { buildProjectSettingsModalUrl } from "@/components/project/settings/project-settings-modal.utils";
// hooks // hooks
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";
@ -23,7 +24,7 @@ function ProjectSettingsLayout({ params }: Route.ComponentProps) {
useEffect(() => { useEffect(() => {
if (projectId) return; if (projectId) return;
if (joinedProjectIds.length > 0) { if (joinedProjectIds.length > 0) {
router.push(`/${workspaceSlug}/settings/projects/${joinedProjectIds[0]}`); router.push(buildProjectSettingsModalUrl(workspaceSlug, joinedProjectIds[0]));
} }
}, [joinedProjectIds, router, workspaceSlug, projectId]); }, [joinedProjectIds, router, workspaceSlug, projectId]);

View File

@ -10,6 +10,7 @@ import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/conten
import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail"; import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail";
import { GlobalModals } from "@/plane-web/components/common/modal/global"; import { GlobalModals } from "@/plane-web/components/common/modal/global";
import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper"; import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper";
import { ProjectSettingsModal } from "@/components/project/settings/project-settings-modal";
import { WorkspaceSettingsModal } from "@/components/workspace/settings/workspace-settings-modal"; import { WorkspaceSettingsModal } from "@/components/workspace/settings/workspace-settings-modal";
import { WorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal"; import { WorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal";
import type { Route } from "./+types/layout"; import type { Route } from "./+types/layout";
@ -24,6 +25,7 @@ export default function WorkspaceLayout(props: Route.ComponentProps) {
<WorkspaceContentWrapper workspaceSlug={workspaceSlug}> <WorkspaceContentWrapper workspaceSlug={workspaceSlug}>
<GlobalModals workspaceSlug={workspaceSlug} /> <GlobalModals workspaceSlug={workspaceSlug} />
<WorkspaceSettingsModal /> <WorkspaceSettingsModal />
<ProjectSettingsModal />
<WorkspaceNotificationsModal /> <WorkspaceNotificationsModal />
<Outlet /> <Outlet />
</WorkspaceContentWrapper> </WorkspaceContentWrapper>

View File

@ -5,13 +5,18 @@
*/ */
import { redirect } from "react-router"; import { redirect } from "react-router";
import {
buildProjectSettingsModalUrl,
getProjectSettingsModalTabFromPath,
} from "@/components/project/settings/project-settings-modal.utils";
import type { Route } from "./+types/project-settings"; import type { Route } from "./+types/project-settings";
export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { export const clientLoader = ({ params }: Route.ClientLoaderArgs) => {
const { workspaceSlug, projectId } = params; const { workspaceSlug, projectId } = params;
const splat = params["*"] || ""; const splat = params["*"] || "";
const destination = `/${workspaceSlug}/settings/projects/${projectId}${splat ? `/${splat}` : ""}/`; const tab = getProjectSettingsModalTabFromPath(splat);
throw redirect(destination);
throw redirect(buildProjectSettingsModalUrl(workspaceSlug, projectId, tab));
}; };
export default function ProjectSettings() { export default function ProjectSettings() {

View File

@ -11,16 +11,13 @@ import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types"; import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
// hooks // hooks
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance"; import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
import { useAppRouter } from "@/hooks/use-app-router";
export const ProjectArchivedEmptyState = observer(function ProjectArchivedEmptyState() { export const ProjectArchivedEmptyState = observer(function ProjectArchivedEmptyState() {
// router const { projectId: routerProjectId } = useParams();
const router = useAppRouter();
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
const projectId = routerProjectId ? routerProjectId.toString() : undefined; const projectId = routerProjectId ? routerProjectId.toString() : undefined;
// plane hooks // plane hooks
const { t } = useTranslation(); const { t } = useTranslation();
@ -57,7 +54,9 @@ export const ProjectArchivedEmptyState = observer(function ProjectArchivedEmptyS
actions={[ actions={[
{ {
label: t("workspace_empty_state.archive_work_items.cta_primary"), label: t("workspace_empty_state.archive_work_items.cta_primary"),
onClick: () => router.push(`/${workspaceSlug}/settings/projects/${projectId}/automations`), onClick: () => {
if (projectId) openProjectSettingsModal(projectId, "automations");
},
disabled: !canPerformEmptyStateActions, disabled: !canPerformEmptyStateActions,
variant: "primary", variant: "primary",
}, },

View File

@ -13,6 +13,7 @@ import { useTranslation } from "@plane/i18n";
import { LinkIcon } from "@plane/propel/icons"; import { LinkIcon } from "@plane/propel/icons";
import type { TContextMenuItem } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { ActionDropdown } from "@plane/ui"; import { ActionDropdown } from "@plane/ui";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -75,7 +76,7 @@ export function ProjectActionsMenu({
title: t("settings"), title: t("settings"),
icon: Settings, icon: Settings,
action: () => { action: () => {
navigate(`/${workspaceSlug}/settings/projects/${project?.id}`); openProjectSettingsModal(project.id);
}, },
}, },
...(!isAuthorized ...(!isAuthorized

View File

@ -26,10 +26,13 @@ import { copyUrlToClipboard, cn, getFileURL, renderFormattedDate } from "@plane/
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// local imports // local imports
import { CoverImage } from "@/components/common/cover-image"; import { CoverImage } from "@/components/common/cover-image";
import {
buildProjectSettingsModalUrl,
openProjectSettingsModal,
} from "@/components/project/settings/project-settings-modal.utils";
import { DeleteProjectModal } from "./delete-project-modal"; import { DeleteProjectModal } from "./delete-project-modal";
import { JoinProjectModal } from "./join-project-modal"; import { JoinProjectModal } from "./join-project-modal";
import { ArchiveRestoreProjectModal } from "./archive-restore-modal"; import { ArchiveRestoreProjectModal } from "./archive-restore-modal";
@ -47,7 +50,6 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
// refs // refs
const projectCardRef = useRef(null); const projectCardRef = useRef(null);
// router // router
const router = useAppRouter();
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
@ -125,7 +127,7 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
const MENU_ITEMS: TContextMenuItem[] = [ const MENU_ITEMS: TContextMenuItem[] = [
{ {
key: "settings", key: "settings",
action: () => router.push(`/${workspaceSlug}/settings/projects/${project.id}`), action: () => openProjectSettingsModal(project.id),
title: "Settings", title: "Settings",
icon: Settings, icon: Settings,
shouldRender: !isArchived && (hasAdminRole || hasMemberRole), shouldRender: !isArchived && (hasAdminRole || hasMemberRole),
@ -339,9 +341,11 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
<Link <Link
className="flex items-center justify-center rounded-sm p-1 text-placeholder hover:bg-layer-1 hover:text-secondary" className="flex items-center justify-center rounded-sm p-1 text-placeholder hover:bg-layer-1 hover:text-secondary"
onClick={(e) => { onClick={(e) => {
e.preventDefault();
e.stopPropagation(); e.stopPropagation();
openProjectSettingsModal(project.id);
}} }}
href={`/${workspaceSlug}/settings/projects/${project.id}`} href={workspaceSlug ? buildProjectSettingsModalUrl(workspaceSlug.toString(), project.id) : "#"}
> >
<Settings className="h-3.5 w-3.5" /> <Settings className="h-3.5 w-3.5" />
</Link> </Link>

View File

@ -0,0 +1,539 @@
import { useEffect, useRef, useState } from "react";
import type { ReactNode } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
import { X } from "lucide-react";
import useSWR from "swr";
// plane imports
import {
EUserPermissions,
EUserPermissionsLevel,
GROUPED_PROJECT_SETTINGS,
PROJECT_SETTINGS,
PROJECT_SETTINGS_CATEGORIES,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ScrollArea } from "@plane/propel/scrollarea";
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
import type { IProject, TProjectSettingsTabs } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components
import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { EstimateRoot } from "@/components/estimates";
import { ProjectSettingsLabelList } from "@/components/labels";
import { ProjectDetailsForm } from "@/components/project/form";
import { ProjectDetailsFormLoader } from "@/components/project/form-loader";
import { ProjectMemberList } from "@/components/project/member-list";
import { ProjectSettingsMemberDefaults } from "@/components/project/project-settings-member-defaults";
import { GeneralProjectSettingsControlSection } from "@/components/project/settings/control-section";
import { ProjectStateRoot } from "@/components/project-states";
import { SettingsSidebarItem } from "@/components/settings/sidebar/item";
import { SettingsHeading } from "@/components/settings/heading";
import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item";
import { ProjectSettingsSidebarHeader } from "@/components/settings/project/sidebar/header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
import { useWorkspace } from "@/hooks/store/use-workspace";
// layouts
import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
// plane web imports
import { CustomAutomationsRoot } from "@/plane-web/components/automations/root";
import { ProjectTeamspaceList } from "@/plane-web/components/projects/teamspaces/teamspace-list";
// services
import { WorkspaceService } from "@/services/workspace.service";
// local imports
import {
closeProjectSettingsModal,
getProjectSettingsModalProjectIdFromSearch,
getProjectSettingsModalTabFromSearch,
openProjectSettingsModal,
PROJECT_SETTINGS_MODAL_EVENT,
type TProjectSettingsModalTab,
} from "./project-settings-modal.utils";
const PROJECT_CATEGORY_I18N_KEYS = {
general: "project_settings_categories.general",
features: "project_settings_categories.features",
"work-structure": "project_settings_categories.work_structure",
execution: "project_settings_categories.execution",
} as const;
const PROJECT_FEATURE_SETTINGS: Record<
Extract<
TProjectSettingsModalTab,
"features_cycles" | "features_modules" | "features_views" | "features_pages" | "features_intake"
>,
{
descriptionKey: string;
featureProperty: keyof IProject;
titleKey: string;
toggleDescriptionKey: string;
toggleTitleKey: string;
}
> = {
features_cycles: {
descriptionKey: "project_settings.features.cycles.description",
featureProperty: "cycle_view",
titleKey: "project_settings.features.cycles.title",
toggleDescriptionKey: "project_settings.features.cycles.toggle_description",
toggleTitleKey: "project_settings.features.cycles.toggle_title",
},
features_modules: {
descriptionKey: "project_settings.features.modules.description",
featureProperty: "module_view",
titleKey: "project_settings.features.modules.title",
toggleDescriptionKey: "project_settings.features.modules.toggle_description",
toggleTitleKey: "project_settings.features.modules.toggle_title",
},
features_views: {
descriptionKey: "project_settings.features.views.description",
featureProperty: "issue_views_view",
titleKey: "project_settings.features.views.title",
toggleDescriptionKey: "project_settings.features.views.toggle_description",
toggleTitleKey: "project_settings.features.views.toggle_title",
},
features_pages: {
descriptionKey: "project_settings.features.pages.description",
featureProperty: "page_view",
titleKey: "project_settings.features.pages.title",
toggleDescriptionKey: "project_settings.features.pages.toggle_description",
toggleTitleKey: "project_settings.features.pages.toggle_title",
},
features_intake: {
descriptionKey: "project_settings.features.intake.description",
featureProperty: "inbox_view",
titleKey: "project_settings.features.intake.title",
toggleDescriptionKey: "project_settings.features.intake.toggle_description",
toggleTitleKey: "project_settings.features.intake.toggle_title",
},
};
const workspaceService = new WorkspaceService();
const getInitialTab = (): TProjectSettingsModalTab => {
if (typeof window === "undefined") return "general";
return getProjectSettingsModalTabFromSearch(window.location.search) ?? "general";
};
const getInitialProjectId = () => {
if (typeof window === "undefined") return undefined;
return getProjectSettingsModalProjectIdFromSearch(window.location.search);
};
const getInitialOpenState = () => {
if (typeof window === "undefined") return false;
return Boolean(getProjectSettingsModalTabFromSearch(window.location.search) && getInitialProjectId());
};
export const ProjectSettingsModal = observer(function ProjectSettingsModal() {
const [activeTab, setActiveTab] = useState<TProjectSettingsModalTab>(getInitialTab);
const [activeProjectId, setActiveProjectId] = useState<string | undefined>(getInitialProjectId);
const [isOpen, setIsOpen] = useState(getInitialOpenState);
// store hooks
const { getProjectById } = useProject();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const projectDetails = getProjectById(activeProjectId);
const workspaceSlug = currentWorkspace?.slug;
const activeTabLabel = PROJECT_SETTINGS[activeTab]?.i18n_label
? t(PROJECT_SETTINGS[activeTab].i18n_label)
: "основные параметры";
useEffect(() => {
const syncFromLocation = () => {
const tab = getProjectSettingsModalTabFromSearch(window.location.search);
const projectId = getProjectSettingsModalProjectIdFromSearch(window.location.search);
setIsOpen(Boolean(tab && projectId));
if (tab) setActiveTab(tab);
setActiveProjectId(projectId);
};
const handleModalEvent = (event: Event) => {
const detail = (event as CustomEvent<{ isOpen: boolean; projectId?: string; tab?: TProjectSettingsModalTab }>)
.detail;
setIsOpen(detail.isOpen);
if (detail.tab) setActiveTab(detail.tab);
if (detail.projectId) setActiveProjectId(detail.projectId);
};
window.addEventListener(PROJECT_SETTINGS_MODAL_EVENT, handleModalEvent);
window.addEventListener("popstate", syncFromLocation);
syncFromLocation();
return () => {
window.removeEventListener(PROJECT_SETTINGS_MODAL_EVENT, handleModalEvent);
window.removeEventListener("popstate", syncFromLocation);
};
}, []);
const handleClose = () => {
closeProjectSettingsModal();
};
const handleSelectItem = (itemKey: TProjectSettingsTabs) => {
if (!activeProjectId) return;
openProjectSettingsModal(activeProjectId, itemKey, true);
};
return (
<ModalCore
isOpen={isOpen}
handleClose={handleClose}
position={EModalPosition.CENTER}
width={EModalWidth.VIIXL}
className="h-[88vh] max-h-[920px] overflow-hidden border-0 bg-[rgba(10,10,14,0.96)] shadow-[0_28px_80px_rgba(0,0,0,0.42)]"
>
{workspaceSlug && activeProjectId ? (
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={activeProjectId}>
<div className="flex h-full min-h-0">
<div className="hidden h-full w-[296px] shrink-0 md:block">
<ProjectSettingsSidebarHeader projectId={activeProjectId} onBack={handleClose} />
<ProjectModalSidebar
activeTab={activeTab}
onSelectItem={handleSelectItem}
projectId={activeProjectId}
workspaceSlug={workspaceSlug}
/>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex shrink-0 items-center justify-between gap-4 px-6 py-5">
<div className="min-w-0">
<div className="text-18 font-semibold text-primary">Настройки проекта</div>
<div className="mt-1 truncate text-12 text-tertiary">
{projectDetails?.name ?? "Project"} / {activeTabLabel}
</div>
</div>
<button
type="button"
onClick={handleClose}
className="grid size-10 flex-shrink-0 place-items-center rounded-full bg-white/6 text-secondary transition hover:bg-white/10 hover:text-primary"
aria-label="Закрыть настройки проекта"
>
<X className="size-5" />
</button>
</div>
<ScrollArea
scrollType="hover"
orientation="vertical"
size="sm"
className="min-h-0 flex-1 overflow-y-auto"
>
<div className="mx-auto w-full max-w-[74rem] px-5 pb-7 lg:px-8">
<ProjectSettingsModalContent
activeTab={activeTab}
projectId={activeProjectId}
workspaceSlug={workspaceSlug}
/>
</div>
</ScrollArea>
</div>
</div>
</ProjectAuthWrapper>
) : null}
</ModalCore>
);
});
type TProjectModalSidebarProps = {
activeTab: TProjectSettingsModalTab;
onSelectItem: (itemKey: TProjectSettingsTabs) => void;
projectId: string;
workspaceSlug: string;
};
const ProjectModalSidebar = observer(function ProjectModalSidebar(props: TProjectModalSidebarProps) {
const { activeTab, onSelectItem, projectId, workspaceSlug } = props;
// store hooks
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
const { data: nodedcWorkspacePolicy } = useSWR(`NODEDC_WORKSPACE_POLICY_${workspaceSlug}`, () =>
workspaceService.getNodeDCWorkspacePolicy(workspaceSlug)
);
const shouldHideMemberSettings = !nodedcWorkspacePolicy || nodedcWorkspacePolicy.managed_by === "launcher";
return (
<ScrollArea
scrollType="hover"
orientation="vertical"
size="sm"
rootClassName="nodedc-settings-sidebar-shell h-[calc(100%-6.75rem)] w-full overflow-y-scroll px-3 py-4"
>
<div className="flex flex-col divide-y divide-white/6">
{PROJECT_SETTINGS_CATEGORIES.map((category) => {
const accessibleItems = GROUPED_PROJECT_SETTINGS[category].filter(
(item) =>
(!shouldHideMemberSettings || item.key !== "members") &&
allowPermissions(item.access, EUserPermissionsLevel.PROJECT, workspaceSlug, projectId)
);
if (accessibleItems.length === 0) return null;
return (
<div key={category} className="shrink-0 py-3.5 first:pt-0 last:pb-0">
<div className="px-3 py-1.5 text-[11px] font-semibold tracking-[0.18em] text-tertiary uppercase">
{t(PROJECT_CATEGORY_I18N_KEYS[category])}
</div>
<div className="flex flex-col">
{accessibleItems.map((item) => (
<SettingsSidebarItem
key={item.key}
as="button"
onClick={() => onSelectItem(item.key)}
isActive={item.key === activeTab}
icon={PROJECT_SETTINGS_ICONS[item.key]}
label={t(item.i18n_label)}
/>
))}
</div>
</div>
);
})}
</div>
</ScrollArea>
);
});
type TProjectSettingsModalContentProps = {
activeTab: TProjectSettingsModalTab;
projectId: string;
workspaceSlug: string;
};
const ProjectSettingsModalContent = observer(function ProjectSettingsModalContent(
props: TProjectSettingsModalContentProps
) {
const { activeTab, projectId, workspaceSlug } = props;
// store hooks
const { getProjectById, updateProject } = useProject();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { t } = useTranslation();
const { data: nodedcWorkspacePolicy } = useSWR(`NODEDC_WORKSPACE_POLICY_${workspaceSlug}`, () =>
workspaceService.getNodeDCWorkspacePolicy(workspaceSlug)
);
// derived values
const projectDetails = getProjectById(projectId);
const pageTitle = projectDetails?.name ? `${projectDetails.name} - Settings` : undefined;
const canPerformProjectAdminActions = allowPermissions(
[EUserPermissions.ADMIN],
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
const canPerformProjectMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE, workspaceSlug);
const canPerformProjectMemberListActions = canPerformProjectMemberActions || isWorkspaceAdmin;
if (activeTab === "general") {
return (
<>
<PageHead title={pageTitle} />
<div className={`w-full ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
{projectDetails ? (
<ProjectDetailsForm
project={projectDetails}
workspaceSlug={workspaceSlug}
projectId={projectId}
isAdmin={canPerformProjectAdminActions}
/>
) : (
<ProjectDetailsFormLoader />
)}
{canPerformProjectAdminActions && <GeneralProjectSettingsControlSection projectId={projectId} />}
</div>
</>
);
}
if (activeTab === "members") {
if (workspaceUserInfo && !canPerformProjectMemberListActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
if (nodedcWorkspacePolicy?.managed_by === "launcher") {
return (
<>
<PageHead title={pageTitle} />
<SettingsHeading title={t("common.members")} />
<section className="border-custom-border-200 bg-custom-background-90 rounded-2xl border p-8">
<p className="text-sm text-custom-text-300 font-semibold tracking-[0.22em] uppercase">
NODE.DC managed project
</p>
<div className="mt-3 max-w-2xl space-y-3">
<h4 className="text-h3-medium">Участники проекта управляются в Launcher.</h4>
<p className="text-custom-text-300 text-body-sm-regular">
Этот workspace подключен к enterprise-контуру NODE.DC. Project-level доступы назначаются в Launcher,
поэтому локальное управление участниками проекта в Task Manager заблокировано.
</p>
</div>
</section>
</>
);
}
return (
<>
<PageHead title={pageTitle} />
<SettingsHeading title={t("common.members")} />
<ProjectSettingsMemberDefaults projectId={projectId} workspaceSlug={workspaceSlug} />
<ProjectTeamspaceList projectId={projectId} workspaceSlug={workspaceSlug} />
<ProjectMemberList projectId={projectId} workspaceSlug={workspaceSlug} />
</>
);
}
if (activeTab in PROJECT_FEATURE_SETTINGS) {
const featureSettings = PROJECT_FEATURE_SETTINGS[activeTab as keyof typeof PROJECT_FEATURE_SETTINGS];
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<>
<PageHead title={pageTitle} />
<section className="w-full">
<SettingsHeading title={t(featureSettings.titleKey)} description={t(featureSettings.descriptionKey)} />
<div className="mt-7">
<ProjectSettingsFeatureControlItem
title={t(featureSettings.toggleTitleKey)}
description={t(featureSettings.toggleDescriptionKey)}
featureProperty={featureSettings.featureProperty}
projectId={projectId}
value={!!projectDetails?.[featureSettings.featureProperty]}
workspaceSlug={workspaceSlug}
/>
</div>
</section>
</>
);
}
if (activeTab === "states") {
if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<>
<PageHead title={pageTitle} />
<div className="w-full">
<SettingsHeading
title={t("project_settings.states.heading")}
description={t("project_settings.states.description")}
/>
<div className="mt-6">
<ProjectStateRoot workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
</div>
</>
);
}
if (activeTab === "labels") {
if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return <ProjectLabelsSettingsContent pageTitle={pageTitle} />;
}
if (activeTab === "estimates") {
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<>
<PageHead title={pageTitle} />
<div className={`w-full ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}>
<EstimateRoot workspaceSlug={workspaceSlug} projectId={projectId} isAdmin={canPerformProjectAdminActions} />
</div>
</>
);
}
if (activeTab === "automations") {
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
const handleChange = async (formData: Partial<IProject>) => {
if (!projectDetails) return;
try {
await updateProject(workspaceSlug, projectId, formData);
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong. Please try again.",
});
}
};
return (
<>
<PageHead title={pageTitle} />
<section className={`w-full ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<SettingsHeading
title={t("project_settings.automations.heading")}
description={t("project_settings.automations.description")}
/>
<div className="mt-6">
<AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} />
</div>
</section>
<CustomAutomationsRoot projectId={projectId} workspaceSlug={workspaceSlug} />
</>
);
}
return null;
});
function ProjectLabelsSettingsContent(props: { pageTitle?: string }) {
const { pageTitle } = props;
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const element = scrollableContainerRef.current;
if (!element) return;
return combine(
autoScrollForElements({
element,
})
);
}, []);
return (
<>
<PageHead title={pageTitle} />
<div ref={scrollableContainerRef} className="size-full">
<ProjectSettingsLabelList />
</div>
</>
);
}

View File

@ -0,0 +1,121 @@
import type { TProjectSettingsTabs } from "@plane/types";
export const PROJECT_SETTINGS_MODAL_QUERY_KEY = "projectSettings";
export const PROJECT_SETTINGS_PROJECT_QUERY_KEY = "projectId";
export const PROJECT_SETTINGS_MODAL_EVENT = "nodedc:project-settings-modal";
export type TProjectSettingsModalTab = TProjectSettingsTabs;
type TProjectSettingsModalEventDetail = {
isOpen: boolean;
projectId?: string;
tab?: TProjectSettingsModalTab;
};
const PROJECT_SETTINGS_MODAL_TABS = new Set<TProjectSettingsModalTab>([
"general",
"members",
"features_cycles",
"features_modules",
"features_views",
"features_pages",
"features_intake",
"states",
"labels",
"estimates",
"automations",
]);
const PROJECT_SETTINGS_PATH_TO_TAB: Array<[string, TProjectSettingsModalTab]> = [
["features/cycles", "features_cycles"],
["features/modules", "features_modules"],
["features/views", "features_views"],
["features/pages", "features_pages"],
["features/intake", "features_intake"],
["features", "features_cycles"],
["members", "members"],
["states", "states"],
["labels", "labels"],
["estimates", "estimates"],
["automations", "automations"],
];
const dispatchProjectSettingsModalEvent = (detail: TProjectSettingsModalEventDetail) => {
window.dispatchEvent(new CustomEvent<TProjectSettingsModalEventDetail>(PROJECT_SETTINGS_MODAL_EVENT, { detail }));
};
export const getProjectSettingsModalTabFromSearch = (search: string): TProjectSettingsModalTab | undefined => {
const value = new URLSearchParams(search).get(PROJECT_SETTINGS_MODAL_QUERY_KEY) as TProjectSettingsModalTab | null;
return value && PROJECT_SETTINGS_MODAL_TABS.has(value) ? value : undefined;
};
export const getProjectSettingsModalProjectIdFromSearch = (search: string): string | undefined => {
const value = new URLSearchParams(search).get(PROJECT_SETTINGS_PROJECT_QUERY_KEY);
return value || undefined;
};
export const getProjectSettingsModalTabFromPath = (path: string | undefined): TProjectSettingsModalTab => {
const normalizedPath = (path ?? "").replace(/^\/+|\/+$/g, "");
if (!normalizedPath) return "general";
const matchedTab = PROJECT_SETTINGS_PATH_TO_TAB.find(([suffix]) => normalizedPath.endsWith(suffix));
return matchedTab?.[1] ?? "general";
};
export const buildProjectSettingsModalSearch = (projectId: string, tab: TProjectSettingsModalTab = "general") => {
const searchParams = new URLSearchParams();
searchParams.set(PROJECT_SETTINGS_MODAL_QUERY_KEY, tab);
searchParams.set(PROJECT_SETTINGS_PROJECT_QUERY_KEY, projectId);
return searchParams.toString();
};
export const buildProjectSettingsModalUrl = (
workspaceSlug: string,
projectId: string,
tab: TProjectSettingsModalTab = "general"
) => `/${workspaceSlug}/?${buildProjectSettingsModalSearch(projectId, tab)}`;
export const setProjectSettingsModalSearch = (
projectId: string,
tab: TProjectSettingsModalTab = "general",
replace = false
) => {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
url.searchParams.set(PROJECT_SETTINGS_MODAL_QUERY_KEY, tab);
url.searchParams.set(PROJECT_SETTINGS_PROJECT_QUERY_KEY, projectId);
window.history[replace ? "replaceState" : "pushState"](window.history.state, "", url);
};
export const clearProjectSettingsModalSearch = () => {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
url.searchParams.delete(PROJECT_SETTINGS_MODAL_QUERY_KEY);
url.searchParams.delete(PROJECT_SETTINGS_PROJECT_QUERY_KEY);
window.history.replaceState(window.history.state, "", url);
};
export const openProjectSettingsModal = (
projectId: string,
tab: TProjectSettingsModalTab = "general",
replace = false
) => {
if (typeof window === "undefined") return;
setProjectSettingsModalSearch(projectId, tab, replace);
dispatchProjectSettingsModalEvent({ isOpen: true, projectId, tab });
};
export const closeProjectSettingsModal = () => {
if (typeof window === "undefined") return;
clearProjectSettingsModalSearch();
dispatchProjectSettingsModalEvent({ isOpen: false });
};

View File

@ -18,11 +18,12 @@ import { useProject } from "@/hooks/store/use-project";
import { useWorkspace } from "@/hooks/store/use-workspace"; import { useWorkspace } from "@/hooks/store/use-workspace";
type Props = { type Props = {
onBack?: () => void;
projectId: string; projectId: string;
}; };
export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSidebarHeader(props: Props) { export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSidebarHeader(props: Props) {
const { projectId } = props; const { onBack, projectId } = props;
// router // router
const router = useAppRouter(); const router = useAppRouter();
// store hooks // store hooks
@ -47,7 +48,14 @@ export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSid
size="base" size="base"
icon={ArrowLeft} icon={ArrowLeft}
className="nodedc-toolbar-icon-button" className="nodedc-toolbar-icon-button"
onClick={() => router.push(`/${currentWorkspace?.slug}/projects/${projectId}/issues/`)} onClick={() => {
if (onBack) {
onBack();
return;
}
router.push(`/${currentWorkspace?.slug}/projects/${projectId}/issues/`);
}}
/> />
<p>{t("project_settings_label")}</p> <p>{t("project_settings_label")}</p>
</div> </div>
@ -57,7 +65,9 @@ export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSid
</div> </div>
<div className="truncate"> <div className="truncate">
<p className="truncate text-body-sm-medium text-primary">{projectDetails?.name}</p> <p className="truncate text-body-sm-medium text-primary">{projectDetails?.name}</p>
<p className="truncate text-caption-md-medium text-tertiary">{t(ROLE_DETAILS[currentProjectRole].i18n_title)}</p> <p className="truncate text-caption-md-medium text-tertiary">
{t(ROLE_DETAILS[currentProjectRole].i18n_title)}
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -32,6 +32,7 @@ import { DEFAULT_TAB_KEY, getTabUrl } from "@/components/navigation/tab-navigati
import { useTabPreferences } from "@/components/navigation/use-tab-preferences"; import { useTabPreferences } from "@/components/navigation/use-tab-preferences";
import { LeaveProjectModal } from "@/components/project/leave-project-modal"; import { LeaveProjectModal } from "@/components/project/leave-project-modal";
import { PublishProjectModal } from "@/components/project/publish-project/modal"; import { PublishProjectModal } from "@/components/project/publish-project/modal";
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
// hooks // hooks
import { useAppTheme } from "@/hooks/store/use-app-theme"; import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useCommandPalette } from "@/hooks/store/use-command-palette";
@ -175,7 +176,7 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
label: t("settings"), label: t("settings"),
icon: <Settings className="h-3.5 w-3.5 stroke-[1.5]" />, icon: <Settings className="h-3.5 w-3.5 stroke-[1.5]" />,
onClick: () => { onClick: () => {
router.push(`/${workspaceSlug}/settings/projects/${project?.id}`); if (project?.id) openProjectSettingsModal(project.id);
}, },
}, },
!isAuthorized !isAuthorized