feat(tasker): open project settings in modal
This commit is contained in:
parent
bf75ce84eb
commit
e49840341b
|
|
@ -25,13 +25,13 @@ import { CycleAppliedFiltersList } from "@/components/cycles/applied-filters";
|
|||
import { CyclesView } from "@/components/cycles/cycles-view";
|
||||
import { CycleCreateUpdateModal } from "@/components/cycles/modal";
|
||||
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";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useCycleFilter } from "@/hooks/store/use-cycle-filter";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
function ProjectCyclesPage({ params }: Route.ComponentProps) {
|
||||
|
|
@ -40,8 +40,6 @@ function ProjectCyclesPage({ params }: Route.ComponentProps) {
|
|||
// store hooks
|
||||
const { currentProjectCycleIds, loader } = useCycle();
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = params;
|
||||
// theme hook
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
|
@ -81,7 +79,7 @@ function ProjectCyclesPage({ params }: Route.ComponentProps) {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.cycle.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
openProjectSettingsModal(projectId, "features_cycles");
|
||||
},
|
||||
disabled: !hasAdminLevelPermission,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,13 @@ import lightIntakeAsset from "@/app/assets/empty-state/disabled-feature/intake-l
|
|||
import { PageHead } from "@/components/core/page-title";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { InboxIssueRoot } from "@/components/inbox";
|
||||
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
function ProjectInboxPage({ params }: Route.ComponentProps) {
|
||||
/// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = params;
|
||||
const searchParams = useSearchParams();
|
||||
const navigationTab = searchParams.get("currentTab");
|
||||
|
|
@ -53,7 +51,7 @@ function ProjectInboxPage({ params }: Route.ComponentProps) {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.inbox.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
openProjectSettingsModal(projectId, "features_intake");
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,15 @@ import lightModulesAsset from "@/app/assets/empty-state/disabled-feature/modules
|
|||
import { PageHead } from "@/components/core/page-title";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
|
||||
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
|
||||
// hooks
|
||||
import { useModuleFilter } from "@/hooks/store/use-module-filter";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
function ProjectModulesPage({ params }: Route.ComponentProps) {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = params;
|
||||
const { projectId } = params;
|
||||
// theme hook
|
||||
const { resolvedTheme } = useTheme();
|
||||
// plane hooks
|
||||
|
|
@ -74,7 +72,7 @@ function ProjectModulesPage({ params }: Route.ComponentProps) {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.module.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
openProjectSettingsModal(projectId, "features_modules");
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import { PageHead } from "@/components/core/page-title";
|
|||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { PagesListRoot } from "@/components/pages/list/root";
|
||||
import { PagesListView } from "@/components/pages/pages-list-view";
|
||||
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web hooks
|
||||
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { Route } from "./+types/page";
|
||||
|
|
@ -36,7 +36,6 @@ const getPageType = (pageType?: string | null): TPageNavigationTabs => {
|
|||
|
||||
function ProjectPagesPage({ params }: Route.ComponentProps) {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const type = searchParams.get("type");
|
||||
const { workspaceSlug, projectId } = params;
|
||||
|
|
@ -65,7 +64,7 @@ function ProjectPagesPage({ params }: Route.ComponentProps) {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.page.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
openProjectSettingsModal(projectId, "features_pages");
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -20,19 +20,17 @@ import lightViewsAsset from "@/app/assets/empty-state/disabled-feature/views-lig
|
|||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
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 { ProjectViewsList } from "@/components/views/views-list";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
function ProjectViewsPage({ params }: Route.ComponentProps) {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = params;
|
||||
const { projectId } = params;
|
||||
// theme hook
|
||||
const { resolvedTheme } = useTheme();
|
||||
// plane hooks
|
||||
|
|
@ -77,7 +75,7 @@ function ProjectViewsPage({ params }: Route.ComponentProps) {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.view.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
openProjectSettingsModal(projectId, "features_views");
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Outlet } from "react-router";
|
||||
import { Outlet, redirect } from "react-router";
|
||||
// components
|
||||
import {
|
||||
buildProjectSettingsModalUrl,
|
||||
getProjectSettingsModalTabFromPath,
|
||||
} from "@/components/project/settings/project-settings-modal.utils";
|
||||
import { getProjectActivePath } from "@/components/settings/helper";
|
||||
import { SettingsMobileNav } from "@/components/settings/mobile/nav";
|
||||
// layouts
|
||||
|
|
@ -16,6 +20,13 @@ import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
|||
import type { Route } from "./+types/layout";
|
||||
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) {
|
||||
const { workspaceSlug, projectId } = params;
|
||||
// router
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Outlet } from "react-router";
|
||||
import { buildProjectSettingsModalUrl } from "@/components/project/settings/project-settings-modal.utils";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
@ -23,7 +24,7 @@ function ProjectSettingsLayout({ params }: Route.ComponentProps) {
|
|||
useEffect(() => {
|
||||
if (projectId) return;
|
||||
if (joinedProjectIds.length > 0) {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${joinedProjectIds[0]}`);
|
||||
router.push(buildProjectSettingsModalUrl(workspaceSlug, joinedProjectIds[0]));
|
||||
}
|
||||
}, [joinedProjectIds, router, workspaceSlug, projectId]);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/conten
|
|||
import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail";
|
||||
import { GlobalModals } from "@/plane-web/components/common/modal/global";
|
||||
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 { WorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal";
|
||||
import type { Route } from "./+types/layout";
|
||||
|
|
@ -24,6 +25,7 @@ export default function WorkspaceLayout(props: Route.ComponentProps) {
|
|||
<WorkspaceContentWrapper workspaceSlug={workspaceSlug}>
|
||||
<GlobalModals workspaceSlug={workspaceSlug} />
|
||||
<WorkspaceSettingsModal />
|
||||
<ProjectSettingsModal />
|
||||
<WorkspaceNotificationsModal />
|
||||
<Outlet />
|
||||
</WorkspaceContentWrapper>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@
|
|||
*/
|
||||
|
||||
import { redirect } from "react-router";
|
||||
import {
|
||||
buildProjectSettingsModalUrl,
|
||||
getProjectSettingsModalTabFromPath,
|
||||
} from "@/components/project/settings/project-settings-modal.utils";
|
||||
import type { Route } from "./+types/project-settings";
|
||||
|
||||
export const clientLoader = ({ params }: Route.ClientLoaderArgs) => {
|
||||
const { workspaceSlug, projectId } = params;
|
||||
const splat = params["*"] || "";
|
||||
const destination = `/${workspaceSlug}/settings/projects/${projectId}${splat ? `/${splat}` : ""}/`;
|
||||
throw redirect(destination);
|
||||
const tab = getProjectSettingsModalTabFromPath(splat);
|
||||
|
||||
throw redirect(buildProjectSettingsModalUrl(workspaceSlug, projectId, tab));
|
||||
};
|
||||
|
||||
export default function ProjectSettings() {
|
||||
|
|
|
|||
|
|
@ -11,16 +11,13 @@ import { EUserPermissionsLevel } from "@plane/constants";
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateDetailed } from "@plane/propel/empty-state";
|
||||
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
|
||||
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
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() {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const { projectId: routerProjectId } = useParams();
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -57,7 +54,9 @@ export const ProjectArchivedEmptyState = observer(function ProjectArchivedEmptyS
|
|||
actions={[
|
||||
{
|
||||
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,
|
||||
variant: "primary",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { LinkIcon } from "@plane/propel/icons";
|
||||
import type { TContextMenuItem } from "@plane/ui";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -75,7 +76,7 @@ export function ProjectActionsMenu({
|
|||
title: t("settings"),
|
||||
icon: Settings,
|
||||
action: () => {
|
||||
navigate(`/${workspaceSlug}/settings/projects/${project?.id}`);
|
||||
openProjectSettingsModal(project.id);
|
||||
},
|
||||
},
|
||||
...(!isAuthorized
|
||||
|
|
|
|||
|
|
@ -26,10 +26,13 @@ import { copyUrlToClipboard, cn, getFileURL, renderFormattedDate } from "@plane/
|
|||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local imports
|
||||
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 { JoinProjectModal } from "./join-project-modal";
|
||||
import { ArchiveRestoreProjectModal } from "./archive-restore-modal";
|
||||
|
|
@ -47,7 +50,6 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
|
|||
// refs
|
||||
const projectCardRef = useRef(null);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
|
@ -125,7 +127,7 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
|
|||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "settings",
|
||||
action: () => router.push(`/${workspaceSlug}/settings/projects/${project.id}`),
|
||||
action: () => openProjectSettingsModal(project.id),
|
||||
title: "Settings",
|
||||
icon: Settings,
|
||||
shouldRender: !isArchived && (hasAdminRole || hasMemberRole),
|
||||
|
|
@ -339,9 +341,11 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
|
|||
<Link
|
||||
className="flex items-center justify-center rounded-sm p-1 text-placeholder hover:bg-layer-1 hover:text-secondary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
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" />
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 });
|
||||
};
|
||||
|
|
@ -18,11 +18,12 @@ import { useProject } from "@/hooks/store/use-project";
|
|||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
|
||||
type Props = {
|
||||
onBack?: () => void;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSidebarHeader(props: Props) {
|
||||
const { projectId } = props;
|
||||
const { onBack, projectId } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
|
|
@ -47,7 +48,14 @@ export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSid
|
|||
size="base"
|
||||
icon={ArrowLeft}
|
||||
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>
|
||||
</div>
|
||||
|
|
@ -57,7 +65,9 @@ export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSid
|
|||
</div>
|
||||
<div className="truncate">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { DEFAULT_TAB_KEY, getTabUrl } from "@/components/navigation/tab-navigati
|
|||
import { useTabPreferences } from "@/components/navigation/use-tab-preferences";
|
||||
import { LeaveProjectModal } from "@/components/project/leave-project-modal";
|
||||
import { PublishProjectModal } from "@/components/project/publish-project/modal";
|
||||
import { openProjectSettingsModal } from "@/components/project/settings/project-settings-modal.utils";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
|
|
@ -175,7 +176,7 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
|
|||
label: t("settings"),
|
||||
icon: <Settings className="h-3.5 w-3.5 stroke-[1.5]" />,
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${project?.id}`);
|
||||
if (project?.id) openProjectSettingsModal(project.id);
|
||||
},
|
||||
},
|
||||
!isAuthorized
|
||||
|
|
|
|||
Loading…
Reference in New Issue