Files
WooNooW/ADDON_MODULE_DESIGN_DECISIONS.md
Dwindi Ramadhana 07020bc0dd 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
2025-12-26 19:19:49 +07:00

617 lines
17 KiB
Markdown

# Addon-Module Integration: Design Decisions
**Date**: December 26, 2025
**Status**: 🎯 Decision Document
---
## 1. Dynamic Categories (RECOMMENDED)
### ❌ Problem with Static Categories
```php
// BAD: Empty categories if no modules use them
public static function get_categories() {
return [
'shipping' => 'Shipping & Fulfillment', // Empty if no shipping modules!
'payments' => 'Payments & Checkout', // Empty if no payment modules!
];
}
```
### ✅ Solution: Dynamic Category Generation
```php
class ModuleRegistry {
/**
* Get categories dynamically from registered modules
*/
public static function get_categories() {
$all_modules = self::get_all_modules();
$categories = [];
// Extract unique categories from modules
foreach ($all_modules as $module) {
$cat = $module['category'] ?? 'other';
if (!isset($categories[$cat])) {
$categories[$cat] = self::get_category_label($cat);
}
}
// Sort by predefined order (if exists), then alphabetically
$order = ['marketing', 'customers', 'products', 'shipping', 'payments', 'analytics', 'other'];
uksort($categories, function($a, $b) use ($order) {
$pos_a = array_search($a, $order);
$pos_b = array_search($b, $order);
if ($pos_a === false) $pos_a = 999;
if ($pos_b === false) $pos_b = 999;
return $pos_a - $pos_b;
});
return $categories;
}
/**
* Get human-readable label for category
*/
private static function get_category_label($category) {
$labels = [
'marketing' => __('Marketing & Sales', 'woonoow'),
'customers' => __('Customer Experience', 'woonoow'),
'products' => __('Products & Inventory', 'woonoow'),
'shipping' => __('Shipping & Fulfillment', 'woonoow'),
'payments' => __('Payments & Checkout', 'woonoow'),
'analytics' => __('Analytics & Reports', 'woonoow'),
'other' => __('Other Extensions', 'woonoow'),
];
return $labels[$category] ?? ucfirst($category);
}
/**
* Group modules by category
*/
public static function get_grouped_modules() {
$all_modules = self::get_all_modules();
$grouped = [];
foreach ($all_modules as $module) {
$cat = $module['category'] ?? 'other';
if (!isset($grouped[$cat])) {
$grouped[$cat] = [];
}
$grouped[$cat][] = $module;
}
return $grouped;
}
}
```
### Benefits
- ✅ No empty categories
- ✅ Addons can define custom categories
- ✅ Single registration point (module only)
- ✅ Auto-sorted by predefined order
---
## 2. Module Settings URL Pattern (RECOMMENDED)
### ❌ Problem with Custom URLs
```php
'settings_url' => '/settings/shipping/biteship', // Conflict risk!
'settings_url' => '/marketing/newsletter', // Inconsistent!
```
### ✅ Solution: Convention-Based Pattern
#### Option A: Standardized Pattern (RECOMMENDED)
```php
// Module registration - NO settings_url needed!
$addons['biteship-shipping'] = [
'id' => 'biteship-shipping',
'name' => 'Biteship Shipping',
'has_settings' => true, // Just a flag!
];
// Auto-generated URL pattern:
// /settings/modules/{module_id}
// Example: /settings/modules/biteship-shipping
```
#### Backend: Auto Route Registration
```php
class ModuleRegistry {
/**
* Register module settings routes automatically
*/
public static function register_settings_routes() {
$modules = self::get_all_modules();
foreach ($modules as $module) {
if (empty($module['has_settings'])) continue;
// Auto-register route: /settings/modules/{module_id}
add_filter('woonoow/spa_routes', function($routes) use ($module) {
$routes[] = [
'path' => "/settings/modules/{$module['id']}",
'component_url' => $module['settings_component'] ?? null,
'title' => sprintf(__('%s Settings', 'woonoow'), $module['label']),
];
return $routes;
});
}
}
}
```
#### Frontend: Automatic Navigation
```tsx
// Modules.tsx - Gear icon auto-links
{module.has_settings && module.enabled && (
<Button
variant="ghost"
size="icon"
onClick={() => navigate(`/settings/modules/${module.id}`)}
>
<Settings className="h-4 w-4" />
</Button>
)}
```
### Benefits
- ✅ No URL conflicts (enforced pattern)
- ✅ Consistent navigation
- ✅ Simpler addon registration
- ✅ Auto-generated breadcrumbs
---
## 3. Form Builder vs Custom HTML (HYBRID APPROACH)
### ✅ Recommended: Provide Both Options
#### Option A: Schema-Based Form Builder (For Simple Settings)
```php
// Addon defines settings schema
add_filter('woonoow/module_settings_schema', function($schemas) {
$schemas['biteship-shipping'] = [
'api_key' => [
'type' => 'text',
'label' => 'API Key',
'description' => 'Your Biteship API key',
'required' => true,
],
'enable_tracking' => [
'type' => 'toggle',
'label' => 'Enable Tracking',
'default' => true,
],
'default_courier' => [
'type' => 'select',
'label' => 'Default Courier',
'options' => [
'jne' => 'JNE',
'jnt' => 'J&T Express',
'sicepat' => 'SiCepat',
],
],
];
return $schemas;
});
```
**Auto-rendered form** - No React needed!
#### Option B: Custom React Component (For Complex Settings)
```php
// Addon provides custom React component
add_filter('woonoow/addon_registry', function($addons) {
$addons['biteship-shipping'] = [
'id' => 'biteship-shipping',
'has_settings' => true,
'settings_component' => plugin_dir_url(__FILE__) . 'dist/Settings.js',
];
return $addons;
});
```
**Full control** - Custom React UI
### Implementation
```php
class ModuleSettingsRenderer {
/**
* Render settings page
*/
public static function render($module_id) {
$module = ModuleRegistry::get_module($module_id);
// Option 1: Has custom component
if (!empty($module['settings_component'])) {
return self::render_custom_component($module);
}
// Option 2: Has schema - auto-generate form
$schema = apply_filters('woonoow/module_settings_schema', []);
if (isset($schema[$module_id])) {
return self::render_schema_form($module_id, $schema[$module_id]);
}
// Option 3: No settings
return ['error' => 'No settings available'];
}
}
```
### Benefits
- ✅ Simple addons use schema (no React needed)
- ✅ Complex addons use custom components
- ✅ Consistent data persistence for both
- ✅ Gradual complexity curve
---
## 4. Settings Data Persistence (STANDARDIZED)
### ✅ Recommended: Unified Settings API
#### Backend: Automatic Persistence
```php
class ModuleSettingsController extends WP_REST_Controller {
/**
* GET /woonoow/v1/modules/{module_id}/settings
*/
public function get_settings($request) {
$module_id = $request['module_id'];
$settings = get_option("woonoow_module_{$module_id}_settings", []);
// Apply defaults from schema
$schema = apply_filters('woonoow/module_settings_schema', []);
if (isset($schema[$module_id])) {
$settings = wp_parse_args($settings, self::get_defaults($schema[$module_id]));
}
return rest_ensure_response($settings);
}
/**
* POST /woonoow/v1/modules/{module_id}/settings
*/
public function update_settings($request) {
$module_id = $request['module_id'];
$new_settings = $request->get_json_params();
// Validate against schema
$schema = apply_filters('woonoow/module_settings_schema', []);
if (isset($schema[$module_id])) {
$validated = self::validate_settings($new_settings, $schema[$module_id]);
if (is_wp_error($validated)) {
return $validated;
}
$new_settings = $validated;
}
// Save
update_option("woonoow_module_{$module_id}_settings", $new_settings);
// Allow addons to react
do_action("woonoow/module_settings_updated/{$module_id}", $new_settings);
return rest_ensure_response(['success' => true]);
}
}
```
#### Frontend: Unified Hook
```tsx
// useModuleSettings.ts
export function useModuleSettings(moduleId: string) {
const queryClient = useQueryClient();
const { data: settings, isLoading } = useQuery({
queryKey: ['module-settings', moduleId],
queryFn: async () => {
const response = await api.get(`/modules/${moduleId}/settings`);
return response;
},
});
const updateSettings = useMutation({
mutationFn: async (newSettings: any) => {
return api.post(`/modules/${moduleId}/settings`, newSettings);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['module-settings', moduleId] });
toast.success('Settings saved');
},
});
return { settings, isLoading, updateSettings };
}
```
#### Addon Usage
```tsx
// Custom settings component
export default function BiteshipSettings() {
const { settings, updateSettings } = useModuleSettings('biteship-shipping');
return (
<SettingsLayout title="Biteship Settings">
<SettingsCard>
<Input
label="API Key"
value={settings?.api_key || ''}
onChange={(e) => updateSettings.mutate({ api_key: e.target.value })}
/>
</SettingsCard>
</SettingsLayout>
);
}
```
### Benefits
- ✅ Consistent storage pattern: `woonoow_module_{id}_settings`
- ✅ Automatic validation (if schema provided)
- ✅ React hook for easy access
- ✅ Action hooks for addon logic
---
## 5. React Extension Pattern (DOCUMENTED)
### ✅ Solution: Window API + Build Externals
#### WooNooW Core Exposes React
```typescript
// admin-spa/src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { useQuery, useMutation } from '@tanstack/react-query';
// Expose for addons
window.WooNooW = {
React,
ReactDOM,
hooks: {
useQuery,
useMutation,
useModuleSettings, // Our custom hook!
},
components: {
SettingsLayout,
SettingsCard,
Button,
Input,
Select,
Switch,
// ... all shadcn components
},
utils: {
api,
toast,
},
};
```
#### Addon Development
```typescript
// addon/src/Settings.tsx
const { React, hooks, components, utils } = window.WooNooW;
const { useModuleSettings } = hooks;
const { SettingsLayout, SettingsCard, Input, Button } = components;
const { toast } = utils;
export default function BiteshipSettings() {
const { settings, updateSettings } = useModuleSettings('biteship-shipping');
const [apiKey, setApiKey] = React.useState(settings?.api_key || '');
const handleSave = () => {
updateSettings.mutate({ api_key: apiKey });
};
return React.createElement(SettingsLayout, { title: 'Biteship Settings' },
React.createElement(SettingsCard, null,
React.createElement(Input, {
label: 'API Key',
value: apiKey,
onChange: (e) => setApiKey(e.target.value),
}),
React.createElement(Button, { onClick: handleSave }, 'Save')
)
);
}
```
#### With JSX (Build Required)
```tsx
// addon/src/Settings.tsx
const { React, hooks, components } = window.WooNooW;
const { useModuleSettings } = hooks;
const { SettingsLayout, SettingsCard, Input, Button } = components;
export default function BiteshipSettings() {
const { settings, updateSettings } = useModuleSettings('biteship-shipping');
return (
<SettingsLayout title="Biteship Settings">
<SettingsCard>
<Input
label="API Key"
value={settings?.api_key || ''}
onChange={(e) => updateSettings.mutate({ api_key: e.target.value })}
/>
</SettingsCard>
</SettingsLayout>
);
}
```
```javascript
// vite.config.js
export default {
build: {
lib: {
entry: 'src/Settings.tsx',
formats: ['es'],
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'window.WooNooW.React',
'react-dom': 'window.WooNooW.ReactDOM',
},
},
},
},
};
```
### Benefits
- ✅ Addons don't bundle React (use ours)
- ✅ Access to all WooNooW components
- ✅ Consistent UI automatically
- ✅ Type safety with TypeScript
---
## 6. Newsletter as Addon Example (RECOMMENDED)
### ✅ Yes, Refactor Newsletter as Built-in Addon
#### Why This is Valuable
1. **Dogfooding** - We use our own addon system
2. **Example** - Best reference for addon developers
3. **Consistency** - Newsletter follows same pattern as external addons
4. **Testing** - Proves the system works
#### Proposed Structure
```
includes/
Modules/
Newsletter/
NewsletterModule.php # Module registration
NewsletterController.php # API endpoints (moved from Api/)
NewsletterSettings.php # Settings schema
admin-spa/src/modules/
Newsletter/
Settings.tsx # Settings page
Subscribers.tsx # Subscribers page
index.ts # Module exports
```
#### Registration Pattern
```php
// includes/Modules/Newsletter/NewsletterModule.php
class NewsletterModule {
public static function register() {
// Register as module
add_filter('woonoow/builtin_modules', function($modules) {
$modules['newsletter'] = [
'id' => 'newsletter',
'label' => __('Newsletter', 'woonoow'),
'description' => __('Email newsletter subscriptions', 'woonoow'),
'category' => 'marketing',
'icon' => 'mail',
'default_enabled' => true,
'has_settings' => true,
'settings_component' => self::get_settings_url(),
];
return $modules;
});
// Register routes (only if enabled)
if (ModuleRegistry::is_enabled('newsletter')) {
self::register_routes();
}
}
private static function register_routes() {
// Settings route
add_filter('woonoow/spa_routes', function($routes) {
$routes[] = [
'path' => '/settings/modules/newsletter',
'component_url' => plugins_url('admin-spa/dist/modules/Newsletter/Settings.js', WOONOOW_FILE),
];
return $routes;
});
// Subscribers route
add_filter('woonoow/spa_routes', function($routes) {
$routes[] = [
'path' => '/marketing/newsletter',
'component_url' => plugins_url('admin-spa/dist/modules/Newsletter/Subscribers.js', WOONOOW_FILE),
];
return $routes;
});
}
}
```
### Benefits
- ✅ Newsletter becomes reference implementation
- ✅ Proves addon system works for complex modules
- ✅ Shows best practices
- ✅ Easier to maintain (follows pattern)
---
## Summary of Decisions
| # | Question | Decision | Rationale |
|---|----------|----------|-----------|
| 1 | Categories | **Dynamic from modules** | No empty categories, single registration |
| 2 | Settings URL | **Pattern: `/settings/modules/{id}`** | No conflicts, consistent, auto-generated |
| 3 | Form Builder | **Hybrid: Schema + Custom** | Simple for basic, flexible for complex |
| 4 | Data Persistence | **Unified API + Hook** | Consistent storage, easy access |
| 5 | React Extension | **Window API + Externals** | No bundling, access to components |
| 6 | Newsletter Refactor | **Yes, as example** | Dogfooding, reference implementation |
---
## Implementation Order
### Phase 1: Foundation
1. ✅ Dynamic category generation
2. ✅ Standardized settings URL pattern
3. ✅ Module settings API endpoints
4.`useModuleSettings` hook
### Phase 2: Form System
1. ✅ Schema-based form renderer
2. ✅ Custom component loader
3. ✅ Settings validation
### Phase 3: UI Enhancement
1. ✅ Search input on Modules page
2. ✅ Category filter pills
3. ✅ Gear icon with auto-routing
### Phase 4: Example
1. ✅ Refactor Newsletter as built-in addon
2. ✅ Document pattern
3. ✅ Create external addon example (Biteship)
---
## Next Steps
**Ready to implement?** We have clear decisions on all 6 questions. Should we:
1. Start with Phase 1 (Foundation)?
2. Create the schema-based form system first?
3. Refactor Newsletter as proof-of-concept?
**Your call!** All design decisions are documented and justified.