ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: администрирование воркспейсов и feature-gates в God Mode

This commit is contained in:
DCCONSTRUCTIONS
2026-04-29 00:38:11 +03:00
parent 7f47b85c36
commit 7bf416ec1f
15 changed files with 923 additions and 40 deletions
@@ -7,18 +7,24 @@
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { useParams } from "react-router";
import useSWR from "swr";
// plane imports
import { EUserPermissionsLevel, GROUPED_WORKSPACE_SETTINGS, WORKSPACE_SETTINGS_CATEGORIES } from "@plane/constants";
import { EUserPermissionsLevel, GROUPED_WORKSPACE_SETTINGS, WORKSPACE_SETTINGS, WORKSPACE_SETTINGS_CATEGORIES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { TWorkspaceSettingsTabs } from "@plane/types";
import { joinUrlPath } from "@plane/utils";
// components
import { SettingsSidebarItem } from "@/components/settings/sidebar/item";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
// services
import { WorkspaceAIService } from "@/services/workspace-ai.service";
// local imports
import { WORKSPACE_SETTINGS_ICONS } from "./item-icon";
const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set(["billing-and-plans"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker"]);
const workspaceAIService = new WorkspaceAIService();
export const WorkspaceSettingsSidebarItemCategories = observer(function WorkspaceSettingsSidebarItemCategories() {
// params
@@ -28,6 +34,15 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
const { allowPermissions } = useUserPermissions();
// translation
const { t } = useTranslation();
// feature gates
const canLoadVoiceTaskerEntitlement =
!!workspaceSlug &&
allowPermissions(WORKSPACE_SETTINGS["ai-voice-tasker"].access, EUserPermissionsLevel.WORKSPACE, workspaceSlug);
const { data: aiSettings } = useSWR(
canLoadVoiceTaskerEntitlement ? `WORKSPACE_AI_SETTINGS_${workspaceSlug}` : null,
() => workspaceAIService.retrieveSettings(workspaceSlug as string)
);
const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true;
return (
<div className="mt-4 flex flex-col divide-y divide-white/6">
@@ -36,6 +51,7 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
const accessibleItems = categoryItems.filter(
(item) =>
!HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) &&
(!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || isVoiceTaskerEntitled) &&
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug)
);
@@ -7,6 +7,7 @@
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { X } from "lucide-react";
import useSWR from "swr";
// plane imports
import {
EUserPermissionsLevel,
@@ -32,6 +33,8 @@ import { WorkspaceDetails } from "@/components/workspace/settings/workspace-deta
// hooks
import { useUserPermissions } from "@/hooks/store/user";
import { useWorkspace } from "@/hooks/store/use-workspace";
// services
import { WorkspaceAIService } from "@/services/workspace-ai.service";
// local imports
import {
closeWorkspaceSettingsModal,
@@ -43,7 +46,9 @@ import {
} from "./workspace-settings-modal.utils";
const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["billing-and-plans"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker"]);
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>(["general", "members", "export", "storage", "webhooks", "ai-voice-tasker"]);
const workspaceAIService = new WorkspaceAIService();
const getInitialTab = (): TWorkspaceSettingsModalTab => {
if (typeof window === "undefined") return "general";
@@ -67,6 +72,14 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
const canLoadVoiceTaskerEntitlement =
!!currentWorkspace?.slug &&
allowPermissions(WORKSPACE_SETTINGS["ai-voice-tasker"].access, EUserPermissionsLevel.WORKSPACE, currentWorkspace.slug);
const { data: aiSettings, isLoading: isVoiceTaskerEntitlementLoading } = useSWR(
canLoadVoiceTaskerEntitlement ? `WORKSPACE_AI_SETTINGS_${currentWorkspace.slug}` : null,
() => workspaceAIService.retrieveSettings(currentWorkspace?.slug as string)
);
const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true;
useEffect(() => {
const syncFromLocation = () => {
@@ -97,6 +110,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
};
}, []);
useEffect(() => {
if (!isOpen || activeTab !== "ai-voice-tasker" || isVoiceTaskerEntitlementLoading) return;
if (!isVoiceTaskerEntitled) openWorkspaceSettingsModal("general", true);
}, [activeTab, isOpen, isVoiceTaskerEntitlementLoading, isVoiceTaskerEntitled]);
const handleClose = () => {
closeWorkspaceSettingsModal();
};
@@ -114,6 +132,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
const renderContent = () => {
if (activeTab === "ai-voice-tasker" && currentWorkspace?.slug) {
if (!isVoiceTaskerEntitled) return <WorkspaceDetails />;
return <AIVoiceTaskerSettingsContent workspaceSlug={currentWorkspace.slug} />;
}
@@ -155,6 +174,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
activeTab={activeTab}
onSelectItem={handleSelectItem}
allowPermissions={allowPermissions}
isVoiceTaskerEntitled={isVoiceTaskerEntitled}
workspaceSlug={currentWorkspace?.slug}
/>
</div>
@@ -189,11 +209,18 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
type TWorkspaceModalSidebarProps = {
activeTab: TWorkspaceSettingsModalTab;
allowPermissions: ReturnType<typeof useUserPermissions>["allowPermissions"];
isVoiceTaskerEntitled: boolean;
onSelectItem: (itemKey: TWorkspaceSettingsTabs, itemHref: string) => void;
workspaceSlug?: string;
};
function WorkspaceModalSidebar({ activeTab, allowPermissions, onSelectItem, workspaceSlug }: TWorkspaceModalSidebarProps) {
function WorkspaceModalSidebar({
activeTab,
allowPermissions,
isVoiceTaskerEntitled,
onSelectItem,
workspaceSlug,
}: TWorkspaceModalSidebarProps) {
const { t } = useTranslation();
return (
@@ -208,6 +235,7 @@ function WorkspaceModalSidebar({ activeTab, allowPermissions, onSelectItem, work
const accessibleItems = GROUPED_WORKSPACE_SETTINGS[category].filter(
(item) =>
!HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) &&
(!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || isVoiceTaskerEntitled) &&
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug)
);