moduels setigns rmeove from frotneend
This commit is contained in:
@@ -4,7 +4,7 @@ Settings Models Admin
|
||||
from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin
|
||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings
|
||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
||||
|
||||
|
||||
@admin.register(SystemSettings)
|
||||
@@ -93,19 +93,6 @@ class AISettingsAdmin(AccountAdminMixin, ModelAdmin):
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
|
||||
@admin.register(ModuleEnableSettings)
|
||||
class ModuleEnableSettingsAdmin(AccountAdminMixin, ModelAdmin):
|
||||
list_display = [
|
||||
'account',
|
||||
'planner_enabled',
|
||||
'writer_enabled',
|
||||
'thinker_enabled',
|
||||
'automation_enabled',
|
||||
'site_builder_enabled',
|
||||
'linker_enabled',
|
||||
'optimizer_enabled',
|
||||
'publisher_enabled',
|
||||
]
|
||||
list_filter = ['planner_enabled', 'writer_enabled', 'thinker_enabled', 'automation_enabled', 'site_builder_enabled', 'linker_enabled', 'optimizer_enabled', 'publisher_enabled']
|
||||
search_fields = ['account__name']
|
||||
|
||||
# ModuleEnableSettings is DEPRECATED - use GlobalModuleSettings instead
|
||||
# GlobalModuleSettings is registered in admin.py (platform-wide, singleton)
|
||||
# Old per-account ModuleEnableSettings model is no longer used
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Serializers for Settings Models
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings
|
||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
||||
from .validators import validate_settings_schema
|
||||
|
||||
|
||||
@@ -58,15 +58,8 @@ class ModuleSettingsSerializer(serializers.ModelSerializer):
|
||||
return value
|
||||
|
||||
|
||||
class ModuleEnableSettingsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ModuleEnableSettings
|
||||
fields = [
|
||||
'id', 'planner_enabled', 'writer_enabled', 'thinker_enabled',
|
||||
'automation_enabled', 'site_builder_enabled', 'linker_enabled',
|
||||
'optimizer_enabled', 'publisher_enabled', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['created_at', 'updated_at', 'account']
|
||||
# ModuleEnableSettingsSerializer is DEPRECATED - use GlobalModuleSettings instead
|
||||
# GlobalModuleSettings is managed via Django Admin only (no API serializer needed)
|
||||
|
||||
|
||||
class AISettingsSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -13,10 +13,11 @@ from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAu
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings
|
||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
||||
from .global_settings_models import GlobalModuleSettings
|
||||
from .settings_serializers import (
|
||||
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
||||
ModuleSettingsSerializer, ModuleEnableSettingsSerializer, AISettingsSerializer
|
||||
ModuleSettingsSerializer, AISettingsSerializer
|
||||
)
|
||||
|
||||
|
||||
@@ -291,31 +292,20 @@ class ModuleSettingsViewSet(AccountModelViewSet):
|
||||
update=extend_schema(tags=['System']),
|
||||
partial_update=extend_schema(tags=['System']),
|
||||
)
|
||||
class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
||||
class ModuleEnableSettingsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
ViewSet for GLOBAL module enable/disable settings (read-only).
|
||||
Returns platform-wide module availability.
|
||||
Only superadmin can modify via Django Admin.
|
||||
Returns platform-wide module availability from GlobalModuleSettings singleton.
|
||||
Only superadmin can modify via Django Admin at /admin/system/globalmodulesettings/.
|
||||
"""
|
||||
queryset = ModuleEnableSettings.objects.all()
|
||||
serializer_class = ModuleEnableSettingsSerializer
|
||||
http_method_names = ['get'] # Read-only
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
throttle_scope = 'system'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
def get_permissions(self):
|
||||
"""Read-only for all authenticated users"""
|
||||
return [IsAuthenticatedAndActive(), HasTenantAccess()]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return empty queryset (not used - we return global settings)"""
|
||||
return ModuleEnableSettings.objects.none()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Return global module settings (platform-wide)"""
|
||||
"""Return global module settings (platform-wide, read-only)"""
|
||||
try:
|
||||
from igny8_core.modules.system.global_settings_models import GlobalModuleSettings
|
||||
global_settings = GlobalModuleSettings.get_instance()
|
||||
|
||||
data = {
|
||||
@@ -334,134 +324,14 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
||||
return success_response(data=data, request=request)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
error=str(e),
|
||||
error=f'Failed to load global module settings: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def retrieve(self, request, pk=None, *args, **kwargs):
|
||||
"""Same as list - return global settings"""
|
||||
return self.list(request)
|
||||
return queryset
|
||||
|
||||
@action(detail=False, methods=['get', 'put'], url_path='current', url_name='current')
|
||||
def get_current(self, request):
|
||||
"""Get or update current account's module enable settings"""
|
||||
if request.method == 'GET':
|
||||
return self.list(request)
|
||||
else:
|
||||
return self.update(request, pk=None)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Get or create module enable settings for current account"""
|
||||
try:
|
||||
account = getattr(request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
if user and hasattr(user, 'account'):
|
||||
account = user.account
|
||||
|
||||
if not account:
|
||||
return error_response(
|
||||
error='Account not found',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Check if table exists (migration might not have been run)
|
||||
try:
|
||||
# Get or create settings for account (one per account)
|
||||
try:
|
||||
settings = ModuleEnableSettings.objects.get(account=account)
|
||||
except ModuleEnableSettings.DoesNotExist:
|
||||
# Create default settings for account
|
||||
settings = ModuleEnableSettings.objects.create(account=account)
|
||||
|
||||
serializer = self.get_serializer(settings)
|
||||
return success_response(data=serializer.data, request=request)
|
||||
except Exception as db_error:
|
||||
# Check if it's a "table does not exist" error
|
||||
error_str = str(db_error)
|
||||
if 'does not exist' in error_str.lower() or 'relation' in error_str.lower():
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"ModuleEnableSettings table does not exist. Migration 0007_add_module_enable_settings needs to be run: {error_str}")
|
||||
return error_response(
|
||||
error='Module enable settings table not found. Please run migration: python manage.py migrate igny8_core_modules_system 0007',
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
request=request
|
||||
)
|
||||
# Re-raise other database errors
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
return error_response(
|
||||
error=f'Failed to load module enable settings: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def retrieve(self, request, pk=None, *args, **kwargs):
|
||||
"""Get module enable settings for current account"""
|
||||
try:
|
||||
account = getattr(request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
if user:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
if not account:
|
||||
return error_response(
|
||||
error='Account not found',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get or create settings for account
|
||||
settings, created = ModuleEnableSettings.objects.get_or_create(account=account)
|
||||
serializer = self.get_serializer(settings)
|
||||
return success_response(data=serializer.data, request=request)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
error=f'Failed to load module enable settings: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def update(self, request, pk=None):
|
||||
"""Update module enable settings for current account"""
|
||||
account = getattr(request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
if user:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
if not account:
|
||||
return error_response(
|
||||
error='Account not found',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get or create settings for account
|
||||
settings = ModuleEnableSettings.get_or_create_for_account(account)
|
||||
serializer = self.get_serializer(settings, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return success_response(data=serializer.data, request=request)
|
||||
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
def partial_update(self, request, pk=None):
|
||||
"""Partial update module enable settings"""
|
||||
return self.update(request, pk)
|
||||
"""Same as list - return global settings (singleton, pk ignored)"""
|
||||
return self.list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
|
||||
@@ -4,7 +4,6 @@ import { HelmetProvider } from "react-helmet-async";
|
||||
import AppLayout from "./layout/AppLayout";
|
||||
import { ScrollToTop } from "./components/common/ScrollToTop";
|
||||
import ProtectedRoute from "./components/auth/ProtectedRoute";
|
||||
import ModuleGuard from "./components/common/ModuleGuard";
|
||||
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
|
||||
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
|
||||
import { useAuthStore } from "./store/authStore";
|
||||
@@ -81,7 +80,6 @@ const Users = lazy(() => import("./pages/Settings/Users"));
|
||||
const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions"));
|
||||
const SystemSettings = lazy(() => import("./pages/Settings/System"));
|
||||
const AccountSettings = lazy(() => import("./pages/Settings/Account"));
|
||||
const ModuleSettings = lazy(() => import("./pages/Settings/Modules"));
|
||||
const AISettings = lazy(() => import("./pages/Settings/AI"));
|
||||
const Plans = lazy(() => import("./pages/Settings/Plans"));
|
||||
const Industries = lazy(() => import("./pages/Settings/Industries"));
|
||||
@@ -142,115 +140,42 @@ export default function App() {
|
||||
|
||||
{/* Planner Module - Redirect dashboard to keywords */}
|
||||
<Route path="/planner" element={<Navigate to="/planner/keywords" replace />} />
|
||||
<Route path="/planner/keywords" element={
|
||||
<ModuleGuard module="planner">
|
||||
<Keywords />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/planner/clusters" element={
|
||||
<ModuleGuard module="planner">
|
||||
<Clusters />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/planner/clusters/:id" element={
|
||||
<ModuleGuard module="planner">
|
||||
<ClusterDetail />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/planner/ideas" element={
|
||||
<ModuleGuard module="planner">
|
||||
<Ideas />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/planner/keywords" element={<Keywords />} />
|
||||
<Route path="/planner/clusters" element={<Clusters />} />
|
||||
<Route path="/planner/clusters/:id" element={<ClusterDetail />} />
|
||||
<Route path="/planner/ideas" element={<Ideas />} />
|
||||
|
||||
{/* Writer Module - Redirect dashboard to tasks */}
|
||||
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
|
||||
<Route path="/writer/tasks" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Tasks />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/tasks" element={<Tasks />} />
|
||||
{/* Writer Content Routes - Order matters: list route must come before detail route */}
|
||||
<Route path="/writer/content" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Content />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/content" element={<Content />} />
|
||||
{/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */}
|
||||
<Route path="/writer/content/:id" element={
|
||||
<ModuleGuard module="writer">
|
||||
<ContentView />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/content/:id" element={<ContentView />} />
|
||||
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
|
||||
<Route path="/writer/images" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Images />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/review" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Review />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/published" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Published />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/images" element={<Images />} />
|
||||
<Route path="/writer/review" element={<Review />} />
|
||||
<Route path="/writer/published" element={<Published />} />
|
||||
|
||||
{/* Automation Module */}
|
||||
<Route path="/automation" element={<AutomationPage />} />
|
||||
|
||||
{/* Linker Module - Redirect dashboard to content */}
|
||||
<Route path="/linker" element={<Navigate to="/linker/content" replace />} />
|
||||
<Route path="/linker/content" element={
|
||||
<ModuleGuard module="linker">
|
||||
<LinkerContentList />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/linker/content" element={<LinkerContentList />} />
|
||||
|
||||
{/* Optimizer Module - Redirect dashboard to content */}
|
||||
<Route path="/optimizer" element={<Navigate to="/optimizer/content" replace />} />
|
||||
<Route path="/optimizer/content" element={
|
||||
<ModuleGuard module="optimizer">
|
||||
<OptimizerContentSelector />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/optimizer/analyze/:id" element={
|
||||
<ModuleGuard module="optimizer">
|
||||
<AnalysisPreview />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/optimizer/content" element={<OptimizerContentSelector />} />
|
||||
<Route path="/optimizer/analyze/:id" element={<AnalysisPreview />} />
|
||||
|
||||
{/* Thinker Module */}
|
||||
{/* Thinker Module - Redirect dashboard to prompts */}
|
||||
<Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} />
|
||||
<Route path="/thinker/prompts" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<Prompts />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/author-profiles" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<AuthorProfiles />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/profile" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<ThinkerProfile />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/strategies" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<Strategies />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/image-testing" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<ImageTesting />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/prompts" element={<Prompts />} />
|
||||
<Route path="/thinker/author-profiles" element={<AuthorProfiles />} />
|
||||
<Route path="/thinker/profile" element={<ThinkerProfile />} />
|
||||
<Route path="/thinker/strategies" element={<Strategies />} />
|
||||
<Route path="/thinker/image-testing" element={<ImageTesting />} />
|
||||
|
||||
{/* Billing Module */}
|
||||
<Route path="/billing" element={<Navigate to="/billing/overview" replace />} />
|
||||
@@ -283,7 +208,6 @@ export default function App() {
|
||||
<Route path="/settings/subscriptions" element={<Subscriptions />} />
|
||||
<Route path="/settings/system" element={<SystemSettings />} />
|
||||
<Route path="/settings/account" element={<AccountSettings />} />
|
||||
<Route path="/settings/modules" element={<ModuleSettings />} />
|
||||
<Route path="/settings/ai" element={<AISettings />} />
|
||||
<Route path="/settings/plans" element={<Plans />} />
|
||||
<Route path="/settings/industries" element={<Industries />} />
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import { isModuleEnabled } from '../../config/modules.config';
|
||||
import { isUpgradeError } from '../../utils/upgrade';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ModuleGuardProps {
|
||||
module: string;
|
||||
@@ -11,31 +7,12 @@ interface ModuleGuardProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleGuard - Protects routes based on module enable status
|
||||
* Redirects to settings page if module is disabled
|
||||
* ModuleGuard - DEPRECATED
|
||||
* Module enable/disable is now controlled ONLY via Django Admin (GlobalModuleSettings)
|
||||
* This component no longer checks module status - all modules are accessible
|
||||
*/
|
||||
export default function ModuleGuard({ module, children, redirectTo = '/settings/modules' }: ModuleGuardProps) {
|
||||
const { moduleEnableSettings, loadModuleEnableSettings, loading } = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Load module enable settings if not already loaded
|
||||
if (!moduleEnableSettings && !loading) {
|
||||
loadModuleEnableSettings();
|
||||
}
|
||||
}, [moduleEnableSettings, loading, loadModuleEnableSettings]);
|
||||
|
||||
// While loading, show children (optimistic rendering)
|
||||
if (loading || !moduleEnableSettings) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Check if module is enabled
|
||||
const enabled = isModuleEnabled(module, moduleEnableSettings as any);
|
||||
|
||||
if (!enabled) {
|
||||
return <Navigate to={redirectTo} replace />;
|
||||
}
|
||||
|
||||
export default function ModuleGuard({ children }: ModuleGuardProps) {
|
||||
// Module filtering removed - all modules always accessible
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,33 +78,17 @@ export function getModuleConfig(moduleName: string): ModuleConfig | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enabled modules
|
||||
* Get all enabled modules (all modules always enabled)
|
||||
*/
|
||||
export function getEnabledModules(moduleEnableSettings?: Record<string, boolean>): ModuleConfig[] {
|
||||
return Object.entries(MODULES)
|
||||
.filter(([key, module]) => {
|
||||
// If moduleEnableSettings provided, use it; otherwise default to enabled
|
||||
if (moduleEnableSettings) {
|
||||
const enabledKey = `${key}_enabled` as keyof typeof moduleEnableSettings;
|
||||
return moduleEnableSettings[enabledKey] !== false; // Default to true if not set
|
||||
}
|
||||
return module.enabled;
|
||||
})
|
||||
.map(([, module]) => module);
|
||||
export function getEnabledModules(): ModuleConfig[] {
|
||||
return Object.values(MODULES).filter(module => module.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module is enabled
|
||||
* Check if a module is enabled (all modules always enabled)
|
||||
*/
|
||||
export function isModuleEnabled(moduleName: string, moduleEnableSettings?: Record<string, boolean>): boolean {
|
||||
export function isModuleEnabled(moduleName: string): boolean {
|
||||
const module = MODULES[moduleName];
|
||||
if (!module) return false;
|
||||
|
||||
if (moduleEnableSettings) {
|
||||
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
|
||||
return moduleEnableSettings[enabledKey] !== false; // Default to true if not set
|
||||
}
|
||||
|
||||
return module.enabled;
|
||||
return module ? module.enabled : false;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,14 +41,7 @@ const AppSidebar: React.FC = () => {
|
||||
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
|
||||
const location = useLocation();
|
||||
const { user, isAuthenticated } = useAuthStore();
|
||||
const { moduleEnableSettings, isModuleEnabled: checkModuleEnabled, loadModuleEnableSettings, loading: settingsLoading } = useSettingsStore();
|
||||
|
||||
// Helper to check if module is enabled - memoized to prevent infinite loops
|
||||
const moduleEnabled = useCallback((moduleName: string): boolean => {
|
||||
if (!moduleEnableSettings) return true; // Default to enabled if not loaded
|
||||
return checkModuleEnabled(moduleName);
|
||||
}, [moduleEnableSettings, checkModuleEnabled]);
|
||||
|
||||
const [openSubmenu, setOpenSubmenu] = useState<{
|
||||
sectionIndex: number;
|
||||
itemIndex: number;
|
||||
@@ -64,31 +57,10 @@ const AppSidebar: React.FC = () => {
|
||||
);
|
||||
|
||||
// Load module enable settings on mount (only once) - but only if user is authenticated
|
||||
useEffect(() => {
|
||||
// Only load if user is authenticated and settings aren't already loaded
|
||||
// Skip for non-module pages to reduce unnecessary calls (e.g., account/billing/signup)
|
||||
const path = location.pathname || '';
|
||||
const isModulePage = [
|
||||
'/planner',
|
||||
'/writer',
|
||||
'/automation',
|
||||
'/thinker',
|
||||
'/linker',
|
||||
'/optimizer',
|
||||
'/publisher',
|
||||
'/dashboard',
|
||||
'/home',
|
||||
].some((p) => path.startsWith(p));
|
||||
|
||||
if (user && isAuthenticated && isModulePage && !moduleEnableSettings && !settingsLoading) {
|
||||
loadModuleEnableSettings().catch((error) => {
|
||||
console.warn('Failed to load module enable settings:', error);
|
||||
});
|
||||
}
|
||||
}, [user, isAuthenticated, location.pathname]); // Only run when user/auth or route changes
|
||||
// REMOVED: Module enable settings functionality - all modules always shown
|
||||
// Module enable/disable is now controlled ONLY via Django Admin (GlobalModuleSettings)
|
||||
|
||||
// Define menu sections with useMemo to prevent recreation on every render
|
||||
// Filter out disabled modules based on module enable settings
|
||||
// New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
|
||||
const menuSections: MenuSection[] = useMemo(() => {
|
||||
// SETUP section items (single items, no dropdowns - submenus shown as in-page navigation)
|
||||
@@ -105,62 +77,50 @@ const AppSidebar: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
// Add Thinker if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('thinker')) {
|
||||
setupItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Thinker",
|
||||
path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation
|
||||
});
|
||||
}
|
||||
// Add Thinker (always shown)
|
||||
setupItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Thinker",
|
||||
path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation
|
||||
});
|
||||
|
||||
// WORKFLOW section items (single items, no dropdowns - submenus shown as in-page navigation)
|
||||
// WORKFLOW section items (all modules always shown)
|
||||
const workflowItems: NavItem[] = [];
|
||||
|
||||
// Add Planner if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('planner')) {
|
||||
workflowItems.push({
|
||||
icon: <ListIcon />,
|
||||
name: "Planner",
|
||||
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
|
||||
});
|
||||
}
|
||||
// Add Planner
|
||||
workflowItems.push({
|
||||
icon: <ListIcon />,
|
||||
name: "Planner",
|
||||
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
|
||||
});
|
||||
|
||||
// Add Writer if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('writer')) {
|
||||
workflowItems.push({
|
||||
icon: <TaskIcon />,
|
||||
name: "Writer",
|
||||
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
|
||||
});
|
||||
}
|
||||
// Add Writer
|
||||
workflowItems.push({
|
||||
icon: <TaskIcon />,
|
||||
name: "Writer",
|
||||
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
|
||||
});
|
||||
|
||||
// Add Automation (always available if Writer is enabled)
|
||||
if (moduleEnabled('writer')) {
|
||||
workflowItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Automation",
|
||||
path: "/automation",
|
||||
});
|
||||
}
|
||||
// Add Automation
|
||||
workflowItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Automation",
|
||||
path: "/automation",
|
||||
});
|
||||
|
||||
// Add Linker if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('linker')) {
|
||||
workflowItems.push({
|
||||
icon: <PlugInIcon />,
|
||||
name: "Linker",
|
||||
path: "/linker/content",
|
||||
});
|
||||
}
|
||||
// Add Linker
|
||||
workflowItems.push({
|
||||
icon: <PlugInIcon />,
|
||||
name: "Linker",
|
||||
path: "/linker/content",
|
||||
});
|
||||
|
||||
// Add Optimizer if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('optimizer')) {
|
||||
workflowItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Optimizer",
|
||||
path: "/optimizer/content",
|
||||
});
|
||||
}
|
||||
// Add Optimizer
|
||||
workflowItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Optimizer",
|
||||
path: "/optimizer/content",
|
||||
});
|
||||
|
||||
return [
|
||||
// Dashboard is standalone (no section header)
|
||||
@@ -243,7 +203,7 @@ const AppSidebar: React.FC = () => {
|
||||
],
|
||||
},
|
||||
];
|
||||
}, [moduleEnabled]);
|
||||
}, []); // No dependencies - always show all modules
|
||||
|
||||
// Combine all sections
|
||||
const allSections = useMemo(() => {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import { MODULES } from '../../config/modules.config';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Switch from '../../components/form/switch/Switch';
|
||||
|
||||
export default function ModuleSettings() {
|
||||
const toast = useToast();
|
||||
const {
|
||||
moduleEnableSettings,
|
||||
loadModuleEnableSettings,
|
||||
updateModuleEnableSettings,
|
||||
loading,
|
||||
} = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
loadModuleEnableSettings();
|
||||
}, [loadModuleEnableSettings]);
|
||||
|
||||
const handleToggle = async (moduleName: string, enabled: boolean) => {
|
||||
try {
|
||||
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
|
||||
await updateModuleEnableSettings({
|
||||
[enabledKey]: enabled,
|
||||
} as any);
|
||||
toast.success(`${MODULES[moduleName]?.name || moduleName} ${enabled ? 'enabled' : 'disabled'}`);
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to update module: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleEnabled = (moduleName: string): boolean => {
|
||||
if (!moduleEnableSettings) return true; // Default to enabled
|
||||
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
|
||||
return moduleEnableSettings[enabledKey] !== false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Module Settings" />
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Module Settings</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">Enable or disable modules for your account</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Loading...</div>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
{Object.entries(MODULES).map(([key, module]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-2xl">{module.icon}</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{module.name}
|
||||
</h3>
|
||||
{module.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{module.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{getModuleEnabled(key) ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
<Switch
|
||||
label=""
|
||||
checked={getModuleEnabled(key)}
|
||||
onChange={(enabled) => handleToggle(key, enabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1810,20 +1810,6 @@ export async function deleteUserSetting(key: string): Promise<void> {
|
||||
}
|
||||
|
||||
// Module Settings
|
||||
export interface ModuleEnableSettings {
|
||||
id: number;
|
||||
planner_enabled: boolean;
|
||||
writer_enabled: boolean;
|
||||
thinker_enabled: boolean;
|
||||
automation_enabled: boolean;
|
||||
site_builder_enabled: boolean;
|
||||
linker_enabled: boolean;
|
||||
optimizer_enabled: boolean;
|
||||
publisher_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ModuleSetting {
|
||||
id: number;
|
||||
module_name: string;
|
||||
@@ -1834,9 +1820,6 @@ export interface ModuleSetting {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// Deduplicate module-enable fetches to prevent 429s for normal users
|
||||
let moduleEnableSettingsInFlight: Promise<ModuleEnableSettings> | null = null;
|
||||
|
||||
export async function fetchModuleSettings(moduleName: string): Promise<ModuleSetting[]> {
|
||||
// fetchAPI extracts data from unified format {success: true, data: [...]}
|
||||
// So response IS the array, not an object with results
|
||||
@@ -1851,28 +1834,6 @@ export async function createModuleSetting(data: { module_name: string; key: stri
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchModuleEnableSettings(): Promise<ModuleEnableSettings> {
|
||||
if (moduleEnableSettingsInFlight) {
|
||||
return moduleEnableSettingsInFlight;
|
||||
}
|
||||
|
||||
moduleEnableSettingsInFlight = fetchAPI('/v1/system/settings/modules/enable/');
|
||||
try {
|
||||
const response = await moduleEnableSettingsInFlight;
|
||||
return response;
|
||||
} finally {
|
||||
moduleEnableSettingsInFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateModuleEnableSettings(data: Partial<ModuleEnableSettings>): Promise<ModuleEnableSettings> {
|
||||
const response = await fetchAPI('/v1/system/settings/modules/enable/', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function updateModuleSetting(moduleName: string, key: string, data: Partial<{ config: Record<string, any>; is_active: boolean }>): Promise<ModuleSetting> {
|
||||
return fetchAPI(`/v1/system/settings/modules/${key}/?module_name=${moduleName}`, {
|
||||
method: 'PUT',
|
||||
|
||||
@@ -13,16 +13,13 @@ import {
|
||||
fetchModuleSettings,
|
||||
createModuleSetting,
|
||||
updateModuleSetting,
|
||||
fetchModuleEnableSettings,
|
||||
updateModuleEnableSettings,
|
||||
AccountSetting,
|
||||
ModuleSetting,
|
||||
ModuleEnableSettings,
|
||||
AccountSettingsError,
|
||||
} from '../services/api';
|
||||
|
||||
// Version for cache busting - increment when structure changes
|
||||
const SETTINGS_STORE_VERSION = 2;
|
||||
const SETTINGS_STORE_VERSION = 4;
|
||||
|
||||
const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => {
|
||||
switch (error.type) {
|
||||
@@ -38,7 +35,6 @@ const getAccountSettingsErrorMessage = (error: AccountSettingsError): string =>
|
||||
interface SettingsState {
|
||||
accountSettings: Record<string, AccountSetting>;
|
||||
moduleSettings: Record<string, Record<string, ModuleSetting>>;
|
||||
moduleEnableSettings: ModuleEnableSettings | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
@@ -48,9 +44,6 @@ interface SettingsState {
|
||||
updateAccountSetting: (key: string, value: any) => Promise<void>;
|
||||
loadModuleSettings: (moduleName: string) => Promise<void>;
|
||||
updateModuleSetting: (moduleName: string, key: string, value: any) => Promise<void>;
|
||||
loadModuleEnableSettings: () => Promise<void>;
|
||||
updateModuleEnableSettings: (data: Partial<ModuleEnableSettings>) => Promise<void>;
|
||||
isModuleEnabled: (moduleName: string) => boolean;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -59,11 +52,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
(set, get) => ({
|
||||
accountSettings: {},
|
||||
moduleSettings: {},
|
||||
moduleEnableSettings: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
_moduleEnableLastFetched: 0 as number | undefined,
|
||||
_moduleEnableInFlight: null as Promise<ModuleEnableSettings> | null,
|
||||
|
||||
loadAccountSettings: async () => {
|
||||
set({ loading: true, error: null });
|
||||
@@ -183,60 +173,10 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
}
|
||||
},
|
||||
|
||||
loadModuleEnableSettings: async () => {
|
||||
const state = get() as any;
|
||||
const now = Date.now();
|
||||
// Use cached value if fetched within last 60s
|
||||
if (state.moduleEnableSettings && state._moduleEnableLastFetched && now - state._moduleEnableLastFetched < 60000) {
|
||||
return;
|
||||
}
|
||||
// Coalesce concurrent calls
|
||||
if (state._moduleEnableInFlight) {
|
||||
await state._moduleEnableInFlight;
|
||||
return;
|
||||
}
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const inFlight = fetchModuleEnableSettings();
|
||||
(state as any)._moduleEnableInFlight = inFlight;
|
||||
const settings = await inFlight;
|
||||
set({ moduleEnableSettings: settings, loading: false, _moduleEnableLastFetched: Date.now() });
|
||||
} catch (error: any) {
|
||||
// On 429/403, avoid loops; cache the failure timestamp and do not retry automatically
|
||||
if (error?.status === 429 || error?.status === 403) {
|
||||
set({ loading: false, _moduleEnableLastFetched: Date.now() });
|
||||
return;
|
||||
}
|
||||
set({ error: error.message, loading: false, _moduleEnableLastFetched: Date.now() });
|
||||
} finally {
|
||||
(get() as any)._moduleEnableInFlight = null;
|
||||
}
|
||||
},
|
||||
|
||||
updateModuleEnableSettings: async (data: Partial<ModuleEnableSettings>) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const settings = await updateModuleEnableSettings(data);
|
||||
set({ moduleEnableSettings: settings, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message, loading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
isModuleEnabled: (moduleName: string): boolean => {
|
||||
const settings = get().moduleEnableSettings;
|
||||
if (!settings) return true; // Default to enabled if not loaded
|
||||
|
||||
const enabledKey = `${moduleName}_enabled` as keyof ModuleEnableSettings;
|
||||
return settings[enabledKey] !== false; // Default to true if not set
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
accountSettings: {},
|
||||
moduleSettings: {},
|
||||
moduleEnableSettings: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -244,23 +184,11 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
}),
|
||||
{
|
||||
name: 'settings-storage',
|
||||
version: SETTINGS_STORE_VERSION, // Add version for cache busting
|
||||
version: SETTINGS_STORE_VERSION,
|
||||
partialize: (state) => ({
|
||||
accountSettings: state.accountSettings,
|
||||
moduleSettings: state.moduleSettings,
|
||||
moduleEnableSettings: state.moduleEnableSettings,
|
||||
}),
|
||||
// Migrate function to handle version changes
|
||||
migrate: (persistedState: any, version: number) => {
|
||||
if (version < SETTINGS_STORE_VERSION) {
|
||||
// Clear module enable settings on version upgrade
|
||||
return {
|
||||
...persistedState,
|
||||
moduleEnableSettings: null,
|
||||
};
|
||||
}
|
||||
return persistedState;
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user