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:
Dwindi Ramadhana
2025-12-26 19:19:49 +07:00
parent 0b2c8a56d6
commit 07020bc0dd
59 changed files with 3891 additions and 12132 deletions

View File

@@ -11,6 +11,7 @@ import { Textarea } from '@/components/ui/textarea';
import { Plus, X } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/api';
import { useModules } from '@/hooks/useModules';
interface SocialLink {
id: string;
@@ -36,6 +37,7 @@ interface ContactData {
}
export default function AppearanceFooter() {
const { isEnabled } = useModules();
const [loading, setLoading] = useState(true);
const [columns, setColumns] = useState('4');
const [style, setStyle] = useState('detailed');
@@ -427,7 +429,9 @@ export default function AppearanceFooter() {
<SelectItem value="menu">Menu Links</SelectItem>
<SelectItem value="contact">Contact Info</SelectItem>
<SelectItem value="social">Social Links</SelectItem>
<SelectItem value="newsletter">Newsletter Form</SelectItem>
{isEnabled('newsletter') && (
<SelectItem value="newsletter">Newsletter Form</SelectItem>
)}
<SelectItem value="custom">Custom HTML</SelectItem>
</SelectContent>
</Select>

View File

@@ -8,6 +8,7 @@ import { Download, Trash2, Mail, Search } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/api';
import { useNavigate } from 'react-router-dom';
import { useModules } from '@/hooks/useModules';
import {
Table,
TableBody,
@@ -21,6 +22,27 @@ export default function NewsletterSubscribers() {
const [searchQuery, setSearchQuery] = useState('');
const queryClient = useQueryClient();
const navigate = useNavigate();
const { isEnabled } = useModules();
if (!isEnabled('newsletter')) {
return (
<SettingsLayout
title="Newsletter Subscribers"
description="Newsletter module is disabled"
>
<div className="bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-900 rounded-lg p-6 text-center">
<Mail className="h-12 w-12 text-yellow-600 mx-auto mb-3" />
<h3 className="font-semibold text-lg mb-2">Newsletter Module Disabled</h3>
<p className="text-sm text-muted-foreground mb-4">
The newsletter module is currently disabled. Enable it in Settings &gt; Modules to use this feature.
</p>
<Button onClick={() => navigate('/settings/modules')}>
Go to Module Settings
</Button>
</div>
</SettingsLayout>
);
}
const { data: subscribersData, isLoading } = useQuery({
queryKey: ['newsletter-subscribers'],

View 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>
);
}