From 029c30ae7060c0e02a259abecb4e95145fb1532b Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Tue, 23 Dec 2025 06:49:38 +0000 Subject: [PATCH] Apply 646095da: Module settings UI fixes with moduleStore --- .../modules/system/settings_views.py | 175 +++--------------- backend/igny8_core/modules/system/urls.py | 4 +- frontend/src/App.tsx | 9 +- frontend/src/layout/AppSidebar.tsx | 119 ++++-------- frontend/src/services/api.ts | 19 ++ frontend/src/store/moduleStore.ts | 59 ++++++ frontend/test-module-settings.html | 69 +++++++ 7 files changed, 223 insertions(+), 231 deletions(-) create mode 100644 frontend/src/store/moduleStore.ts create mode 100644 frontend/test-module-settings.html diff --git a/backend/igny8_core/modules/system/settings_views.py b/backend/igny8_core/modules/system/settings_views.py index c4d8d47f..db4e68f1 100644 --- a/backend/igny8_core/modules/system/settings_views.py +++ b/backend/igny8_core/modules/system/settings_views.py @@ -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 ) @@ -288,166 +289,48 @@ class ModuleSettingsViewSet(AccountModelViewSet): @extend_schema_view( list=extend_schema(tags=['System']), retrieve=extend_schema(tags=['System']), - update=extend_schema(tags=['System']), - partial_update=extend_schema(tags=['System']), ) -class ModuleEnableSettingsViewSet(AccountModelViewSet): +class ModuleEnableSettingsViewSet(viewsets.ViewSet): """ - ViewSet for managing module enable/disable settings - Unified API Standard v1.0 compliant - One record per account - Read access: All authenticated users - Write access: Admins/Owners only + ViewSet for GLOBAL module enable/disable settings (read-only, public). + Returns platform-wide module availability from GlobalModuleSettings singleton. + No authentication required - these are public platform-wide settings. + Only superadmin can modify via Django Admin at /admin/system/globalmodulesettings/. """ - queryset = ModuleEnableSettings.objects.all() - serializer_class = ModuleEnableSettingsSerializer - authentication_classes = [JWTAuthentication] + authentication_classes = [] + permission_classes = [] throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] - def get_permissions(self): - """ - Allow read access to all authenticated users, - but restrict write access to admins/owners - """ - if self.action in ['list', 'retrieve', 'get_current']: - permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - else: - permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] - return [permission() for permission in permission_classes] - - def get_queryset(self): - """Get module enable settings for current account""" - # Return queryset filtered by account - but list() will handle get_or_create - queryset = super().get_queryset() - # Filter by account if available - account = getattr(self.request, 'account', None) - if not account: - user = getattr(self.request, 'user', None) - if user: - account = getattr(user, 'account', None) - if account: - queryset = queryset.filter(account=account) - 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""" + """Return global module settings (platform-wide, read-only)""" try: - account = getattr(request, 'account', None) - if not account: - user = getattr(request, 'user', None) - if user and hasattr(user, 'account'): - account = user.account + global_settings = GlobalModuleSettings.get_instance() - 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 + data = { + 'id': 1, + 'planner_enabled': global_settings.planner_enabled, + 'writer_enabled': global_settings.writer_enabled, + 'thinker_enabled': global_settings.thinker_enabled, + 'automation_enabled': global_settings.automation_enabled, + 'site_builder_enabled': global_settings.site_builder_enabled, + 'linker_enabled': global_settings.linker_enabled, + 'optimizer_enabled': global_settings.optimizer_enabled, + 'publisher_enabled': global_settings.publisher_enabled, + 'created_at': global_settings.created_at.isoformat() if global_settings.created_at else None, + 'updated_at': global_settings.updated_at.isoformat() if global_settings.updated_at else None, + } + return success_response(data=data, request=request) except Exception as e: - import traceback - error_trace = traceback.format_exc() return error_response( - error=f'Failed to load module enable settings: {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): - """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( diff --git a/backend/igny8_core/modules/system/urls.py b/backend/igny8_core/modules/system/urls.py index 9c1508b2..aa2a08e4 100644 --- a/backend/igny8_core/modules/system/urls.py +++ b/backend/igny8_core/modules/system/urls.py @@ -52,11 +52,9 @@ integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({ # Custom view for module enable settings to avoid URL routing conflict with ModuleSettingsViewSet # This must be defined as a custom path BEFORE router.urls to ensure it matches first -# The update method handles pk=None correctly, so we can use as_view +# Read-only viewset - only GET is supported (modification via Django Admin only) module_enable_viewset = ModuleEnableSettingsViewSet.as_view({ 'get': 'list', - 'put': 'update', - 'patch': 'partial_update', }) urlpatterns = [ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c77041ab..ec5620e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import { AwsAdminGuard } from "./components/auth/AwsAdminGuard"; import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay"; import LoadingStateMonitor from "./components/common/LoadingStateMonitor"; import { useAuthStore } from "./store/authStore"; +import { useModuleStore } from "./store/moduleStore"; // Auth pages - loaded immediately (needed for login) import SignIn from "./pages/AuthPages/SignIn"; @@ -117,7 +118,13 @@ const Components = lazy(() => import("./pages/Components")); export default function App() { - // All session validation removed - API interceptor handles authentication + const { isAuthenticated } = useAuthStore(); + const { loadModuleSettings } = useModuleStore(); + + // Load global module settings immediately on mount (public endpoint, no auth required) + useEffect(() => { + loadModuleSettings(); + }, [loadModuleSettings]); return ( <> diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 7076d19b..804d6567 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -23,12 +23,14 @@ import SidebarWidget from "./SidebarWidget"; import { APP_VERSION } from "../config/version"; import { useAuthStore } from "../store/authStore"; import { useSettingsStore } from "../store/settingsStore"; +import { useModuleStore } from "../store/moduleStore"; type NavItem = { name: string; icon: React.ReactNode; path?: string; subItems?: { name: string; path: string; pro?: boolean; new?: boolean }[]; + adminOnly?: boolean; }; type MenuSection = { @@ -40,17 +42,8 @@ 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(); + const { isModuleEnabled, settings: moduleSettings } = useModuleStore(); - // Show admin menu only for aws-admin account users - const isAwsAdminAccount = Boolean(user?.account?.slug === 'aws-admin'); - - // 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; @@ -65,33 +58,9 @@ const AppSidebar: React.FC = () => { [location.pathname] ); - // 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 - // 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 + // Module visibility is controlled by GlobalModuleSettings (Django Admin only) const menuSections: MenuSection[] = useMemo(() => { // SETUP section items (single items, no dropdowns - submenus shown as in-page navigation) const setupItems: NavItem[] = [ @@ -100,15 +69,19 @@ const AppSidebar: React.FC = () => { name: "Add Keywords", path: "/setup/add-keywords", }, - { + ]; + + // Add Sites (Site Builder) if enabled + if (isModuleEnabled('site_builder')) { + setupItems.push({ icon: , name: "Sites", path: "/sites", // Submenus shown as in-page navigation - }, - ]; + }); + } - // Add Thinker if enabled (single item, no dropdown) - if (moduleEnabled('thinker')) { + // Add Thinker if enabled + if (isModuleEnabled('thinker')) { setupItems.push({ icon: , name: "Thinker", @@ -116,11 +89,11 @@ const AppSidebar: React.FC = () => { }); } - // WORKFLOW section items (single items, no dropdowns - submenus shown as in-page navigation) + // WORKFLOW section items (conditionally shown based on global settings) const workflowItems: NavItem[] = []; - // Add Planner if enabled (single item, no dropdown) - if (moduleEnabled('planner')) { + // Add Planner if enabled + if (isModuleEnabled('planner')) { workflowItems.push({ icon: , name: "Planner", @@ -128,8 +101,8 @@ const AppSidebar: React.FC = () => { }); } - // Add Writer if enabled (single item, no dropdown) - if (moduleEnabled('writer')) { + // Add Writer if enabled + if (isModuleEnabled('writer')) { workflowItems.push({ icon: , name: "Writer", @@ -137,8 +110,8 @@ const AppSidebar: React.FC = () => { }); } - // Add Automation (always available if Writer is enabled) - if (moduleEnabled('writer')) { + // Add Automation if enabled + if (isModuleEnabled('automation')) { workflowItems.push({ icon: , name: "Automation", @@ -146,8 +119,8 @@ const AppSidebar: React.FC = () => { }); } - // Add Linker if enabled (single item, no dropdown) - if (moduleEnabled('linker')) { + // Add Linker if enabled + if (isModuleEnabled('linker')) { workflowItems.push({ icon: , name: "Linker", @@ -155,8 +128,8 @@ const AppSidebar: React.FC = () => { }); } - // Add Optimizer if enabled (single item, no dropdown) - if (moduleEnabled('optimizer')) { + // Add Optimizer if enabled + if (isModuleEnabled('optimizer')) { workflowItems.push({ icon: , name: "Optimizer", @@ -217,12 +190,10 @@ const AppSidebar: React.FC = () => { name: "Profile Settings", path: "/settings/profile", }, - // Integration is admin-only; hide for non-privileged users (handled in render) { icon: , - name: "Integration", + name: "AI Model Settings", path: "/settings/integration", - adminOnly: true, }, { icon: , @@ -247,34 +218,12 @@ const AppSidebar: React.FC = () => { ], }, ]; - }, [moduleEnabled]); + }, [isModuleEnabled, moduleSettings]); // Re-run when settings change - // Admin section - only shown for aws-admin account users - const adminSection: MenuSection = useMemo(() => ({ - label: "ADMIN", - items: [ - { - icon: , - name: "System Dashboard", - path: "/admin/dashboard", - }, - ], - }), []); - - // Combine all sections, including admin if user is in aws-admin account + // Combine all sections const allSections = useMemo(() => { - const baseSections = menuSections.map(section => { - // Filter adminOnly items for non-system users - const filteredItems = section.items.filter((item: any) => { - if ((item as any).adminOnly && !isAwsAdminAccount) return false; - return true; - }); - return { ...section, items: filteredItems }; - }); - return isAwsAdminAccount - ? [...baseSections, adminSection] - : baseSections; - }, [isAwsAdminAccount, menuSections, adminSection]); + return menuSections; + }, [menuSections]); useEffect(() => { const currentPath = location.pathname; @@ -355,7 +304,15 @@ const AppSidebar: React.FC = () => { const renderMenuItems = (items: NavItem[], sectionIndex: number) => (
    - {items.map((nav, itemIndex) => ( + {items + .filter((nav) => { + // Filter out admin-only items for non-admin users + if (nav.adminOnly && user?.role !== 'admin' && !user?.is_staff) { + return false; + } + return true; + }) + .map((nav, itemIndex) => (
  • {nav.subItems ? ( +
    + + + +