feat: Implement centralized module management system
- Add ModuleRegistry for managing built-in modules (newsletter, wishlist, affiliate, subscription, licensing) - Add ModulesController REST API for module enable/disable - Create Modules settings page with category grouping and toggle controls - Integrate module checks across admin-spa and customer-spa - Add useModules hook for both SPAs to check module status - Hide newsletter from footer builder when module disabled - Hide wishlist features when module disabled (product cards, account menu, wishlist page) - Protect wishlist API endpoints with module checks - Auto-update navigation tree when modules toggled - Clean up obsolete documentation files - Add comprehensive documentation: - MODULE_SYSTEM_IMPLEMENTATION.md - MODULE_INTEGRATION_SUMMARY.md - ADDON_MODULE_INTEGRATION.md (proposal) - ADDON_MODULE_DESIGN_DECISIONS.md (design doc) - FEATURE_ROADMAP.md - SHIPPING_INTEGRATION.md Module system provides: - Centralized enable/disable for all features - Automatic navigation updates - Frontend/backend integration - Foundation for addon-module unification
This commit is contained in:
180
admin-spa/src/routes/Settings/Modules.tsx
Normal file
180
admin-spa/src/routes/Settings/Modules.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api';
|
||||
import { SettingsLayout } from './components/SettingsLayout';
|
||||
import { SettingsCard } from './components/SettingsCard';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { RefreshCw, Mail, Heart, Users, RefreshCcw, Key } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { __ } from '@/lib/i18n';
|
||||
|
||||
interface Module {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
enabled: boolean;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
interface ModulesData {
|
||||
modules: Record<string, Module>;
|
||||
grouped: {
|
||||
marketing: Module[];
|
||||
customers: Module[];
|
||||
products: Module[];
|
||||
};
|
||||
}
|
||||
|
||||
export default function Modules() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: modulesData, isLoading } = useQuery<ModulesData>({
|
||||
queryKey: ['modules'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/modules');
|
||||
// api.get returns JSON directly, not wrapped in .data
|
||||
return response as ModulesData;
|
||||
},
|
||||
});
|
||||
|
||||
const toggleModule = useMutation({
|
||||
mutationFn: async ({ moduleId, enabled }: { moduleId: string; enabled: boolean }) => {
|
||||
return api.post('/modules/toggle', { module_id: moduleId, enabled });
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['modules'] });
|
||||
toast.success(
|
||||
variables.enabled
|
||||
? __('Module enabled successfully')
|
||||
: __('Module disabled successfully')
|
||||
);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || __('Failed to toggle module'));
|
||||
},
|
||||
});
|
||||
|
||||
const getIcon = (iconName: string) => {
|
||||
const icons: Record<string, any> = {
|
||||
mail: Mail,
|
||||
heart: Heart,
|
||||
users: Users,
|
||||
'refresh-cw': RefreshCcw,
|
||||
key: Key,
|
||||
};
|
||||
const Icon = icons[iconName] || Mail;
|
||||
return <Icon className="h-5 w-5" />;
|
||||
};
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
marketing: __('Marketing & Sales'),
|
||||
customers: __('Customer Experience'),
|
||||
products: __('Products & Inventory'),
|
||||
};
|
||||
return labels[category] || category;
|
||||
};
|
||||
|
||||
const categories = ['marketing', 'customers', 'products'];
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Module Management')}
|
||||
description={__('Enable or disable features to customize your store')}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{/* Info Card */}
|
||||
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900 rounded-lg p-4 mb-6">
|
||||
<div className="text-sm space-y-2">
|
||||
<p className="text-blue-900 dark:text-blue-100">
|
||||
{__(
|
||||
'Modules allow you to enable only the features you need. Disabling unused modules improves performance and reduces clutter in your admin panel.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
💡 {__('Tip: When you disable a module, its menu items and settings will be hidden from the admin panel, and its features will be disabled on the frontend.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Module Categories */}
|
||||
{categories.map((category) => {
|
||||
const modules = modulesData?.grouped[category as keyof typeof modulesData.grouped] || [];
|
||||
|
||||
if (modules.length === 0) return null;
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
key={category}
|
||||
title={getCategoryLabel(category)}
|
||||
description={__('Manage modules in this category')}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{modules.map((module) => (
|
||||
<div
|
||||
key={module.id}
|
||||
className="flex items-start gap-4 p-4 border rounded-lg bg-card hover:bg-accent/5 transition-colors"
|
||||
>
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`p-3 rounded-lg ${
|
||||
module.enabled
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{getIcon(module.icon)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-medium text-sm">{module.label}</h3>
|
||||
{module.enabled && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{__('Active')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
{module.description}
|
||||
</p>
|
||||
|
||||
{/* Features List */}
|
||||
{module.features && module.features.length > 0 && (
|
||||
<ul className="space-y-1">
|
||||
{module.features.map((feature, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="text-xs text-muted-foreground flex items-center gap-2"
|
||||
>
|
||||
<span className="text-primary">•</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Toggle Switch */}
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
checked={module.enabled}
|
||||
onCheckedChange={(enabled) =>
|
||||
toggleModule.mutate({ moduleId: module.id, enabled })
|
||||
}
|
||||
disabled={toggleModule.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
})}
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user