moduels setigns rmeove from frotneend
This commit is contained in:
@@ -4,7 +4,7 @@ Settings Models Admin
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from unfold.admin import ModelAdmin
|
from unfold.admin import ModelAdmin
|
||||||
from igny8_core.admin.base import AccountAdminMixin
|
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)
|
@admin.register(SystemSettings)
|
||||||
@@ -93,19 +93,6 @@ class AISettingsAdmin(AccountAdminMixin, ModelAdmin):
|
|||||||
get_account_display.short_description = 'Account'
|
get_account_display.short_description = 'Account'
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ModuleEnableSettings)
|
# ModuleEnableSettings is DEPRECATED - use GlobalModuleSettings instead
|
||||||
class ModuleEnableSettingsAdmin(AccountAdminMixin, ModelAdmin):
|
# GlobalModuleSettings is registered in admin.py (platform-wide, singleton)
|
||||||
list_display = [
|
# Old per-account ModuleEnableSettings model is no longer used
|
||||||
'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']
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Serializers for Settings Models
|
Serializers for Settings Models
|
||||||
"""
|
"""
|
||||||
from rest_framework import serializers
|
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
|
from .validators import validate_settings_schema
|
||||||
|
|
||||||
|
|
||||||
@@ -58,15 +58,8 @@ class ModuleSettingsSerializer(serializers.ModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ModuleEnableSettingsSerializer(serializers.ModelSerializer):
|
# ModuleEnableSettingsSerializer is DEPRECATED - use GlobalModuleSettings instead
|
||||||
class Meta:
|
# GlobalModuleSettings is managed via Django Admin only (no API serializer needed)
|
||||||
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']
|
|
||||||
|
|
||||||
|
|
||||||
class AISettingsSerializer(serializers.ModelSerializer):
|
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.pagination import CustomPageNumberPagination
|
||||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
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 (
|
from .settings_serializers import (
|
||||||
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
||||||
ModuleSettingsSerializer, ModuleEnableSettingsSerializer, AISettingsSerializer
|
ModuleSettingsSerializer, AISettingsSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -291,31 +292,20 @@ class ModuleSettingsViewSet(AccountModelViewSet):
|
|||||||
update=extend_schema(tags=['System']),
|
update=extend_schema(tags=['System']),
|
||||||
partial_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).
|
ViewSet for GLOBAL module enable/disable settings (read-only).
|
||||||
Returns platform-wide module availability.
|
Returns platform-wide module availability from GlobalModuleSettings singleton.
|
||||||
Only superadmin can modify via Django Admin.
|
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]
|
authentication_classes = [JWTAuthentication]
|
||||||
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
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):
|
def list(self, request, *args, **kwargs):
|
||||||
"""Return global module settings (platform-wide)"""
|
"""Return global module settings (platform-wide, read-only)"""
|
||||||
try:
|
try:
|
||||||
from igny8_core.modules.system.global_settings_models import GlobalModuleSettings
|
|
||||||
global_settings = GlobalModuleSettings.get_instance()
|
global_settings = GlobalModuleSettings.get_instance()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@@ -334,134 +324,14 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
|||||||
return success_response(data=data, request=request)
|
return success_response(data=data, request=request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return error_response(
|
return error_response(
|
||||||
error=str(e),
|
error=f'Failed to load global module settings: {str(e)}',
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
def retrieve(self, request, pk=None, *args, **kwargs):
|
def retrieve(self, request, pk=None, *args, **kwargs):
|
||||||
"""Same as list - return global settings"""
|
"""Same as list - return global settings (singleton, pk ignored)"""
|
||||||
return self.list(request)
|
return self.list(request, *args, **kwargs)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { HelmetProvider } from "react-helmet-async";
|
|||||||
import AppLayout from "./layout/AppLayout";
|
import AppLayout from "./layout/AppLayout";
|
||||||
import { ScrollToTop } from "./components/common/ScrollToTop";
|
import { ScrollToTop } from "./components/common/ScrollToTop";
|
||||||
import ProtectedRoute from "./components/auth/ProtectedRoute";
|
import ProtectedRoute from "./components/auth/ProtectedRoute";
|
||||||
import ModuleGuard from "./components/common/ModuleGuard";
|
|
||||||
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
|
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
|
||||||
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
|
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
|
||||||
import { useAuthStore } from "./store/authStore";
|
import { useAuthStore } from "./store/authStore";
|
||||||
@@ -81,7 +80,6 @@ const Users = lazy(() => import("./pages/Settings/Users"));
|
|||||||
const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions"));
|
const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions"));
|
||||||
const SystemSettings = lazy(() => import("./pages/Settings/System"));
|
const SystemSettings = lazy(() => import("./pages/Settings/System"));
|
||||||
const AccountSettings = lazy(() => import("./pages/Settings/Account"));
|
const AccountSettings = lazy(() => import("./pages/Settings/Account"));
|
||||||
const ModuleSettings = lazy(() => import("./pages/Settings/Modules"));
|
|
||||||
const AISettings = lazy(() => import("./pages/Settings/AI"));
|
const AISettings = lazy(() => import("./pages/Settings/AI"));
|
||||||
const Plans = lazy(() => import("./pages/Settings/Plans"));
|
const Plans = lazy(() => import("./pages/Settings/Plans"));
|
||||||
const Industries = lazy(() => import("./pages/Settings/Industries"));
|
const Industries = lazy(() => import("./pages/Settings/Industries"));
|
||||||
@@ -142,115 +140,42 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Planner Module - Redirect dashboard to keywords */}
|
{/* Planner Module - Redirect dashboard to keywords */}
|
||||||
<Route path="/planner" element={<Navigate to="/planner/keywords" replace />} />
|
<Route path="/planner" element={<Navigate to="/planner/keywords" replace />} />
|
||||||
<Route path="/planner/keywords" element={
|
<Route path="/planner/keywords" element={<Keywords />} />
|
||||||
<ModuleGuard module="planner">
|
<Route path="/planner/clusters" element={<Clusters />} />
|
||||||
<Keywords />
|
<Route path="/planner/clusters/:id" element={<ClusterDetail />} />
|
||||||
</ModuleGuard>
|
<Route path="/planner/ideas" element={<Ideas />} />
|
||||||
} />
|
|
||||||
<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>
|
|
||||||
} />
|
|
||||||
|
|
||||||
{/* Writer Module - Redirect dashboard to tasks */}
|
{/* Writer Module - Redirect dashboard to tasks */}
|
||||||
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
|
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
|
||||||
<Route path="/writer/tasks" element={
|
<Route path="/writer/tasks" element={<Tasks />} />
|
||||||
<ModuleGuard module="writer">
|
|
||||||
<Tasks />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
{/* Writer Content Routes - Order matters: list route must come before detail route */}
|
{/* Writer Content Routes - Order matters: list route must come before detail route */}
|
||||||
<Route path="/writer/content" element={
|
<Route path="/writer/content" element={<Content />} />
|
||||||
<ModuleGuard module="writer">
|
|
||||||
<Content />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
{/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */}
|
{/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */}
|
||||||
<Route path="/writer/content/:id" element={
|
<Route path="/writer/content/:id" element={<ContentView />} />
|
||||||
<ModuleGuard module="writer">
|
|
||||||
<ContentView />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
|
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
|
||||||
<Route path="/writer/images" element={
|
<Route path="/writer/images" element={<Images />} />
|
||||||
<ModuleGuard module="writer">
|
<Route path="/writer/review" element={<Review />} />
|
||||||
<Images />
|
<Route path="/writer/published" element={<Published />} />
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
<Route path="/writer/review" element={
|
|
||||||
<ModuleGuard module="writer">
|
|
||||||
<Review />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
<Route path="/writer/published" element={
|
|
||||||
<ModuleGuard module="writer">
|
|
||||||
<Published />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
|
|
||||||
{/* Automation Module */}
|
{/* Automation Module */}
|
||||||
<Route path="/automation" element={<AutomationPage />} />
|
<Route path="/automation" element={<AutomationPage />} />
|
||||||
|
|
||||||
{/* Linker Module - Redirect dashboard to content */}
|
{/* Linker Module - Redirect dashboard to content */}
|
||||||
<Route path="/linker" element={<Navigate to="/linker/content" replace />} />
|
<Route path="/linker" element={<Navigate to="/linker/content" replace />} />
|
||||||
<Route path="/linker/content" element={
|
<Route path="/linker/content" element={<LinkerContentList />} />
|
||||||
<ModuleGuard module="linker">
|
|
||||||
<LinkerContentList />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
|
|
||||||
{/* Optimizer Module - Redirect dashboard to content */}
|
{/* Optimizer Module - Redirect dashboard to content */}
|
||||||
<Route path="/optimizer" element={<Navigate to="/optimizer/content" replace />} />
|
<Route path="/optimizer" element={<Navigate to="/optimizer/content" replace />} />
|
||||||
<Route path="/optimizer/content" element={
|
<Route path="/optimizer/content" element={<OptimizerContentSelector />} />
|
||||||
<ModuleGuard module="optimizer">
|
<Route path="/optimizer/analyze/:id" element={<AnalysisPreview />} />
|
||||||
<OptimizerContentSelector />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
<Route path="/optimizer/analyze/:id" element={
|
|
||||||
<ModuleGuard module="optimizer">
|
|
||||||
<AnalysisPreview />
|
|
||||||
</ModuleGuard>
|
|
||||||
} />
|
|
||||||
|
|
||||||
{/* Thinker Module */}
|
|
||||||
{/* Thinker Module - Redirect dashboard to prompts */}
|
{/* Thinker Module - Redirect dashboard to prompts */}
|
||||||
<Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} />
|
<Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} />
|
||||||
<Route path="/thinker/prompts" element={
|
<Route path="/thinker/prompts" element={<Prompts />} />
|
||||||
<ModuleGuard module="thinker">
|
<Route path="/thinker/author-profiles" element={<AuthorProfiles />} />
|
||||||
<Prompts />
|
<Route path="/thinker/profile" element={<ThinkerProfile />} />
|
||||||
</ModuleGuard>
|
<Route path="/thinker/strategies" element={<Strategies />} />
|
||||||
} />
|
<Route path="/thinker/image-testing" element={<ImageTesting />} />
|
||||||
<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>
|
|
||||||
} />
|
|
||||||
|
|
||||||
{/* Billing Module */}
|
{/* Billing Module */}
|
||||||
<Route path="/billing" element={<Navigate to="/billing/overview" replace />} />
|
<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/subscriptions" element={<Subscriptions />} />
|
||||||
<Route path="/settings/system" element={<SystemSettings />} />
|
<Route path="/settings/system" element={<SystemSettings />} />
|
||||||
<Route path="/settings/account" element={<AccountSettings />} />
|
<Route path="/settings/account" element={<AccountSettings />} />
|
||||||
<Route path="/settings/modules" element={<ModuleSettings />} />
|
|
||||||
<Route path="/settings/ai" element={<AISettings />} />
|
<Route path="/settings/ai" element={<AISettings />} />
|
||||||
<Route path="/settings/plans" element={<Plans />} />
|
<Route path="/settings/plans" element={<Plans />} />
|
||||||
<Route path="/settings/industries" element={<Industries />} />
|
<Route path="/settings/industries" element={<Industries />} />
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { ReactNode, useEffect } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
|
||||||
import { useSettingsStore } from '../../store/settingsStore';
|
|
||||||
import { isModuleEnabled } from '../../config/modules.config';
|
|
||||||
import { isUpgradeError } from '../../utils/upgrade';
|
|
||||||
|
|
||||||
interface ModuleGuardProps {
|
interface ModuleGuardProps {
|
||||||
module: string;
|
module: string;
|
||||||
@@ -11,31 +7,12 @@ interface ModuleGuardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ModuleGuard - Protects routes based on module enable status
|
* ModuleGuard - DEPRECATED
|
||||||
* Redirects to settings page if module is disabled
|
* 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) {
|
export default function ModuleGuard({ children }: ModuleGuardProps) {
|
||||||
const { moduleEnableSettings, loadModuleEnableSettings, loading } = useSettingsStore();
|
// Module filtering removed - all modules always accessible
|
||||||
|
|
||||||
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 />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
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[] {
|
export function getEnabledModules(): ModuleConfig[] {
|
||||||
return Object.entries(MODULES)
|
return Object.values(MODULES).filter(module => module.enabled);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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];
|
const module = MODULES[moduleName];
|
||||||
if (!module) return false;
|
return module ? module.enabled : false;
|
||||||
|
|
||||||
if (moduleEnableSettings) {
|
|
||||||
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
|
|
||||||
return moduleEnableSettings[enabledKey] !== false; // Default to true if not set
|
|
||||||
}
|
|
||||||
|
|
||||||
return module.enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,6 @@ const AppSidebar: React.FC = () => {
|
|||||||
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
|
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, isAuthenticated } = useAuthStore();
|
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<{
|
const [openSubmenu, setOpenSubmenu] = useState<{
|
||||||
sectionIndex: number;
|
sectionIndex: number;
|
||||||
@@ -64,31 +57,10 @@ const AppSidebar: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Load module enable settings on mount (only once) - but only if user is authenticated
|
// Load module enable settings on mount (only once) - but only if user is authenticated
|
||||||
useEffect(() => {
|
// REMOVED: Module enable settings functionality - all modules always shown
|
||||||
// Only load if user is authenticated and settings aren't already loaded
|
// Module enable/disable is now controlled ONLY via Django Admin (GlobalModuleSettings)
|
||||||
// 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
|
|
||||||
|
|
||||||
// Define menu sections with useMemo to prevent recreation on every render
|
// 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
|
// New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
|
||||||
const menuSections: MenuSection[] = useMemo(() => {
|
const menuSections: MenuSection[] = useMemo(() => {
|
||||||
// SETUP section items (single items, no dropdowns - submenus shown as in-page navigation)
|
// 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)
|
// Add Thinker (always shown)
|
||||||
if (moduleEnabled('thinker')) {
|
setupItems.push({
|
||||||
setupItems.push({
|
icon: <BoltIcon />,
|
||||||
icon: <BoltIcon />,
|
name: "Thinker",
|
||||||
name: "Thinker",
|
path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation
|
||||||
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[] = [];
|
const workflowItems: NavItem[] = [];
|
||||||
|
|
||||||
// Add Planner if enabled (single item, no dropdown)
|
// Add Planner
|
||||||
if (moduleEnabled('planner')) {
|
workflowItems.push({
|
||||||
workflowItems.push({
|
icon: <ListIcon />,
|
||||||
icon: <ListIcon />,
|
name: "Planner",
|
||||||
name: "Planner",
|
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
|
||||||
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Writer if enabled (single item, no dropdown)
|
// Add Writer
|
||||||
if (moduleEnabled('writer')) {
|
workflowItems.push({
|
||||||
workflowItems.push({
|
icon: <TaskIcon />,
|
||||||
icon: <TaskIcon />,
|
name: "Writer",
|
||||||
name: "Writer",
|
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
|
||||||
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Automation (always available if Writer is enabled)
|
// Add Automation
|
||||||
if (moduleEnabled('writer')) {
|
workflowItems.push({
|
||||||
workflowItems.push({
|
icon: <BoltIcon />,
|
||||||
icon: <BoltIcon />,
|
name: "Automation",
|
||||||
name: "Automation",
|
path: "/automation",
|
||||||
path: "/automation",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Linker if enabled (single item, no dropdown)
|
// Add Linker
|
||||||
if (moduleEnabled('linker')) {
|
workflowItems.push({
|
||||||
workflowItems.push({
|
icon: <PlugInIcon />,
|
||||||
icon: <PlugInIcon />,
|
name: "Linker",
|
||||||
name: "Linker",
|
path: "/linker/content",
|
||||||
path: "/linker/content",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Optimizer if enabled (single item, no dropdown)
|
// Add Optimizer
|
||||||
if (moduleEnabled('optimizer')) {
|
workflowItems.push({
|
||||||
workflowItems.push({
|
icon: <BoltIcon />,
|
||||||
icon: <BoltIcon />,
|
name: "Optimizer",
|
||||||
name: "Optimizer",
|
path: "/optimizer/content",
|
||||||
path: "/optimizer/content",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// Dashboard is standalone (no section header)
|
// Dashboard is standalone (no section header)
|
||||||
@@ -243,7 +203,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [moduleEnabled]);
|
}, []); // No dependencies - always show all modules
|
||||||
|
|
||||||
// Combine all sections
|
// Combine all sections
|
||||||
const allSections = useMemo(() => {
|
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
|
// 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 {
|
export interface ModuleSetting {
|
||||||
id: number;
|
id: number;
|
||||||
module_name: string;
|
module_name: string;
|
||||||
@@ -1834,9 +1820,6 @@ export interface ModuleSetting {
|
|||||||
updated_at: string;
|
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[]> {
|
export async function fetchModuleSettings(moduleName: string): Promise<ModuleSetting[]> {
|
||||||
// fetchAPI extracts data from unified format {success: true, data: [...]}
|
// fetchAPI extracts data from unified format {success: true, data: [...]}
|
||||||
// So response IS the array, not an object with results
|
// 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> {
|
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}`, {
|
return fetchAPI(`/v1/system/settings/modules/${key}/?module_name=${moduleName}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -13,16 +13,13 @@ import {
|
|||||||
fetchModuleSettings,
|
fetchModuleSettings,
|
||||||
createModuleSetting,
|
createModuleSetting,
|
||||||
updateModuleSetting,
|
updateModuleSetting,
|
||||||
fetchModuleEnableSettings,
|
|
||||||
updateModuleEnableSettings,
|
|
||||||
AccountSetting,
|
AccountSetting,
|
||||||
ModuleSetting,
|
ModuleSetting,
|
||||||
ModuleEnableSettings,
|
|
||||||
AccountSettingsError,
|
AccountSettingsError,
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
|
|
||||||
// Version for cache busting - increment when structure changes
|
// Version for cache busting - increment when structure changes
|
||||||
const SETTINGS_STORE_VERSION = 2;
|
const SETTINGS_STORE_VERSION = 4;
|
||||||
|
|
||||||
const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => {
|
const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => {
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
@@ -38,7 +35,6 @@ const getAccountSettingsErrorMessage = (error: AccountSettingsError): string =>
|
|||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
accountSettings: Record<string, AccountSetting>;
|
accountSettings: Record<string, AccountSetting>;
|
||||||
moduleSettings: Record<string, Record<string, ModuleSetting>>;
|
moduleSettings: Record<string, Record<string, ModuleSetting>>;
|
||||||
moduleEnableSettings: ModuleEnableSettings | null;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
@@ -48,9 +44,6 @@ interface SettingsState {
|
|||||||
updateAccountSetting: (key: string, value: any) => Promise<void>;
|
updateAccountSetting: (key: string, value: any) => Promise<void>;
|
||||||
loadModuleSettings: (moduleName: string) => Promise<void>;
|
loadModuleSettings: (moduleName: string) => Promise<void>;
|
||||||
updateModuleSetting: (moduleName: string, key: string, value: any) => 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;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +52,8 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
accountSettings: {},
|
accountSettings: {},
|
||||||
moduleSettings: {},
|
moduleSettings: {},
|
||||||
moduleEnableSettings: null,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
_moduleEnableLastFetched: 0 as number | undefined,
|
|
||||||
_moduleEnableInFlight: null as Promise<ModuleEnableSettings> | null,
|
|
||||||
|
|
||||||
loadAccountSettings: async () => {
|
loadAccountSettings: async () => {
|
||||||
set({ loading: true, error: null });
|
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: () => {
|
reset: () => {
|
||||||
set({
|
set({
|
||||||
accountSettings: {},
|
accountSettings: {},
|
||||||
moduleSettings: {},
|
moduleSettings: {},
|
||||||
moduleEnableSettings: null,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
@@ -244,23 +184,11 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
version: SETTINGS_STORE_VERSION, // Add version for cache busting
|
version: SETTINGS_STORE_VERSION,
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
accountSettings: state.accountSettings,
|
accountSettings: state.accountSettings,
|
||||||
moduleSettings: state.moduleSettings,
|
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