**Implementation Plan Created: IMPLEMENTATION_PLAN_META_COMPAT.md** Following all documentation guidelines: - ADDON_BRIDGE_PATTERN.md (3-level strategy) - ADDON_DEVELOPMENT_GUIDE.md (hook system) - ADDON_REACT_INTEGRATION.md (React exposure) - METABOX_COMPAT.md (compatibility requirements) **Key Principles:** 1. ✅ Zero addon dependencies in core 2. ✅ Listen to WP/WooCommerce hooks (NOT WooNooW-specific) 3. ✅ Community does NOTHING extra 4. ❌ Do NOT support specific plugins 5. ❌ Do NOT integrate plugins into core **3 Phases:** Phase 1: Backend API Enhancement (2-3 days) - Add get_order_meta_data() / get_product_meta_data() - Add update_order_meta_data() / update_product_meta_data() - Expose meta in API responses - Add filters: woonoow/order_allowed_private_meta - Add filters: woonoow/order_updatable_meta - Add filters: woonoow/order_api_data - Add actions: woonoow/order_updated Phase 2: Frontend Components (3-4 days) - Create MetaFields.tsx component (generic field renderer) - Create useMetaFields.ts hook (registry access) - Update Orders/Edit.tsx to include meta fields - Update Products/Edit.tsx to include meta fields - Support all field types: text, textarea, number, select, checkbox Phase 3: PHP Registry System (2-3 days) - Create MetaFieldsRegistry.php - Add action: woonoow/register_meta_fields - Auto-register fields to allowed meta lists - Localize to JavaScript (window.WooNooWMetaFields) - Initialize in Plugin.php **Testing Plan:** - WooCommerce Shipment Tracking plugin - Advanced Custom Fields (ACF) - Custom metabox plugins - Meta data save/update - Field registration **Timeline:** 8-12 days (1.5-2 weeks) **Success Criteria:** ✅ Plugins using standard WP/WooCommerce meta work automatically ✅ No special integration needed ✅ Meta fields visible and editable ✅ Zero coupling with specific plugins ✅ Community does NOTHING extra Ready to start implementation!
641 lines
18 KiB
Markdown
641 lines
18 KiB
Markdown
# Implementation Plan: Level 1 Meta Compatibility
|
|
|
|
## Objective
|
|
Make WooNooW listen to ALL standard WordPress/WooCommerce hooks for custom meta fields automatically.
|
|
|
|
## Principles (From Documentation Review)
|
|
|
|
### From ADDON_BRIDGE_PATTERN.md:
|
|
1. ✅ WooNooW Core = Zero addon dependencies
|
|
2. ✅ We listen to WP/WooCommerce hooks (NOT WooNooW-specific)
|
|
3. ✅ Community does NOTHING extra
|
|
4. ❌ We do NOT support specific plugins
|
|
5. ❌ We do NOT integrate plugins into core
|
|
|
|
### From ADDON_DEVELOPMENT_GUIDE.md:
|
|
1. ✅ Hook system for functional extensions
|
|
2. ✅ Zero coupling with core
|
|
3. ✅ WordPress-style filters and actions
|
|
|
|
### From ADDON_REACT_INTEGRATION.md:
|
|
1. ✅ Expose React runtime on window
|
|
2. ✅ Support vanilla JS/jQuery addons
|
|
3. ✅ No build process required for simple addons
|
|
|
|
---
|
|
|
|
## Implementation Strategy
|
|
|
|
### Phase 1: Backend API Enhancement (2-3 days)
|
|
|
|
#### 1.1 OrdersController - Expose Meta Data
|
|
|
|
**File:** `includes/Api/OrdersController.php`
|
|
|
|
**Changes:**
|
|
```php
|
|
public static function show(WP_REST_Request $req) {
|
|
$order = wc_get_order($id);
|
|
|
|
// ... existing data ...
|
|
|
|
// Expose meta data (Level 1 compatibility)
|
|
$meta_data = self::get_order_meta_data($order);
|
|
$data['meta'] = $meta_data;
|
|
|
|
// Allow plugins to modify response
|
|
$data = apply_filters('woonoow/order_api_data', $data, $order, $req);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
}
|
|
|
|
/**
|
|
* Get order meta data for API exposure
|
|
* Filters out internal meta unless explicitly allowed
|
|
*/
|
|
private static function get_order_meta_data($order) {
|
|
$meta_data = [];
|
|
|
|
foreach ($order->get_meta_data() as $meta) {
|
|
$key = $meta->key;
|
|
$value = $meta->value;
|
|
|
|
// Skip internal WooCommerce meta (starts with _wc_)
|
|
if (strpos($key, '_wc_') === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Public meta (no underscore) - always expose
|
|
if (strpos($key, '_') !== 0) {
|
|
$meta_data[$key] = $value;
|
|
continue;
|
|
}
|
|
|
|
// Private meta (starts with _) - check if allowed
|
|
$allowed_private = apply_filters('woonoow/order_allowed_private_meta', [
|
|
// Common shipping tracking fields
|
|
'_tracking_number',
|
|
'_tracking_provider',
|
|
'_tracking_url',
|
|
'_shipment_tracking_items',
|
|
'_wc_shipment_tracking_items',
|
|
|
|
// Allow plugins to add their meta
|
|
], $order);
|
|
|
|
if (in_array($key, $allowed_private, true)) {
|
|
$meta_data[$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $meta_data;
|
|
}
|
|
```
|
|
|
|
**Update Method:**
|
|
```php
|
|
public static function update(WP_REST_Request $req) {
|
|
$order = wc_get_order($id);
|
|
$data = $req->get_json_params();
|
|
|
|
// ... existing update logic ...
|
|
|
|
// Update custom meta fields (Level 1 compatibility)
|
|
if (isset($data['meta']) && is_array($data['meta'])) {
|
|
self::update_order_meta_data($order, $data['meta']);
|
|
}
|
|
|
|
$order->save();
|
|
|
|
// Allow plugins to perform additional updates
|
|
do_action('woonoow/order_updated', $order, $data, $req);
|
|
|
|
return new WP_REST_Response(['success' => true], 200);
|
|
}
|
|
|
|
/**
|
|
* Update order meta data from API
|
|
*/
|
|
private static function update_order_meta_data($order, $meta_updates) {
|
|
// Get allowed updatable meta keys
|
|
$allowed = apply_filters('woonoow/order_updatable_meta', [
|
|
'_tracking_number',
|
|
'_tracking_provider',
|
|
'_tracking_url',
|
|
// Allow plugins to add their meta
|
|
], $order);
|
|
|
|
foreach ($meta_updates as $key => $value) {
|
|
// Public meta (no underscore) - always allow
|
|
if (strpos($key, '_') !== 0) {
|
|
$order->update_meta_data($key, $value);
|
|
continue;
|
|
}
|
|
|
|
// Private meta - check if allowed
|
|
if (in_array($key, $allowed, true)) {
|
|
$order->update_meta_data($key, $value);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 1.2 ProductsController - Expose Meta Data
|
|
|
|
**File:** `includes/Api/ProductsController.php`
|
|
|
|
**Changes:** (Same pattern as OrdersController)
|
|
```php
|
|
public static function get_product(WP_REST_Request $request) {
|
|
$product = wc_get_product($id);
|
|
|
|
// ... existing data ...
|
|
|
|
// Expose meta data (Level 1 compatibility)
|
|
$meta_data = self::get_product_meta_data($product);
|
|
$data['meta'] = $meta_data;
|
|
|
|
// Allow plugins to modify response
|
|
$data = apply_filters('woonoow/product_api_data', $data, $product, $request);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
}
|
|
|
|
private static function get_product_meta_data($product) {
|
|
// Same logic as orders
|
|
}
|
|
|
|
public static function update_product(WP_REST_Request $request) {
|
|
// ... existing logic ...
|
|
|
|
if (isset($data['meta']) && is_array($data['meta'])) {
|
|
self::update_product_meta_data($product, $data['meta']);
|
|
}
|
|
|
|
do_action('woonoow/product_updated', $product, $data, $request);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 2: Frontend Components (3-4 days)
|
|
|
|
#### 2.1 MetaFields Component
|
|
|
|
**File:** `admin-spa/src/components/MetaFields.tsx`
|
|
|
|
**Purpose:** Generic component to display/edit meta fields
|
|
|
|
```tsx
|
|
interface MetaField {
|
|
key: string;
|
|
label: string;
|
|
type: 'text' | 'textarea' | 'number' | 'select' | 'date' | 'checkbox';
|
|
options?: Array<{value: string; label: string}>;
|
|
section?: string;
|
|
description?: string;
|
|
placeholder?: string;
|
|
}
|
|
|
|
interface MetaFieldsProps {
|
|
meta: Record<string, any>;
|
|
fields: MetaField[];
|
|
onChange: (key: string, value: any) => void;
|
|
readOnly?: boolean;
|
|
}
|
|
|
|
export function MetaFields({ meta, fields, onChange, readOnly }: MetaFieldsProps) {
|
|
if (fields.length === 0) return null;
|
|
|
|
// Group fields by section
|
|
const sections = fields.reduce((acc, field) => {
|
|
const section = field.section || 'Additional Fields';
|
|
if (!acc[section]) acc[section] = [];
|
|
acc[section].push(field);
|
|
return acc;
|
|
}, {} as Record<string, MetaField[]>);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{Object.entries(sections).map(([section, sectionFields]) => (
|
|
<Card key={section}>
|
|
<CardHeader>
|
|
<CardTitle>{section}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{sectionFields.map(field => (
|
|
<div key={field.key}>
|
|
<Label htmlFor={field.key}>
|
|
{field.label}
|
|
{field.description && (
|
|
<span className="text-xs text-muted-foreground ml-2">
|
|
{field.description}
|
|
</span>
|
|
)}
|
|
</Label>
|
|
|
|
{field.type === 'text' && (
|
|
<Input
|
|
id={field.key}
|
|
value={meta[field.key] || ''}
|
|
onChange={(e) => onChange(field.key, e.target.value)}
|
|
disabled={readOnly}
|
|
placeholder={field.placeholder}
|
|
/>
|
|
)}
|
|
|
|
{field.type === 'textarea' && (
|
|
<Textarea
|
|
id={field.key}
|
|
value={meta[field.key] || ''}
|
|
onChange={(e) => onChange(field.key, e.target.value)}
|
|
disabled={readOnly}
|
|
placeholder={field.placeholder}
|
|
rows={4}
|
|
/>
|
|
)}
|
|
|
|
{field.type === 'number' && (
|
|
<Input
|
|
id={field.key}
|
|
type="number"
|
|
value={meta[field.key] || ''}
|
|
onChange={(e) => onChange(field.key, e.target.value)}
|
|
disabled={readOnly}
|
|
placeholder={field.placeholder}
|
|
/>
|
|
)}
|
|
|
|
{field.type === 'select' && field.options && (
|
|
<Select
|
|
value={meta[field.key] || ''}
|
|
onValueChange={(value) => onChange(field.key, value)}
|
|
disabled={readOnly}
|
|
>
|
|
<SelectTrigger id={field.key}>
|
|
<SelectValue placeholder={field.placeholder || 'Select...'} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{field.options.map(opt => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
|
|
{field.type === 'checkbox' && (
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={field.key}
|
|
checked={!!meta[field.key]}
|
|
onCheckedChange={(checked) => onChange(field.key, checked)}
|
|
disabled={readOnly}
|
|
/>
|
|
<label htmlFor={field.key} className="text-sm cursor-pointer">
|
|
{field.placeholder || 'Enable'}
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 2.2 useMetaFields Hook
|
|
|
|
**File:** `admin-spa/src/hooks/useMetaFields.ts`
|
|
|
|
**Purpose:** Hook to get registered meta fields from global registry
|
|
|
|
```tsx
|
|
interface MetaFieldsRegistry {
|
|
orders: MetaField[];
|
|
products: MetaField[];
|
|
}
|
|
|
|
// Global registry exposed by PHP
|
|
declare global {
|
|
interface Window {
|
|
WooNooWMetaFields?: MetaFieldsRegistry;
|
|
}
|
|
}
|
|
|
|
export function useMetaFields(type: 'orders' | 'products'): MetaField[] {
|
|
const [fields, setFields] = useState<MetaField[]>([]);
|
|
|
|
useEffect(() => {
|
|
// Get fields from global registry (set by PHP)
|
|
const registry = window.WooNooWMetaFields || { orders: [], products: [] };
|
|
setFields(registry[type] || []);
|
|
|
|
// Listen for dynamic field registration
|
|
const handleFieldsUpdated = (e: CustomEvent) => {
|
|
if (e.detail.type === type) {
|
|
setFields(e.detail.fields);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('woonoow:meta_fields_updated', handleFieldsUpdated as EventListener);
|
|
|
|
return () => {
|
|
window.removeEventListener('woonoow:meta_fields_updated', handleFieldsUpdated as EventListener);
|
|
};
|
|
}, [type]);
|
|
|
|
return fields;
|
|
}
|
|
```
|
|
|
|
#### 2.3 Integration in Order Edit
|
|
|
|
**File:** `admin-spa/src/routes/Orders/Edit.tsx`
|
|
|
|
```tsx
|
|
import { MetaFields } from '@/components/MetaFields';
|
|
import { useMetaFields } from '@/hooks/useMetaFields';
|
|
|
|
export default function OrderEdit() {
|
|
const { id } = useParams();
|
|
const metaFields = useMetaFields('orders');
|
|
|
|
const [formData, setFormData] = useState({
|
|
// ... existing fields ...
|
|
meta: {},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (orderQ.data) {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
meta: orderQ.data.meta || {},
|
|
}));
|
|
}
|
|
}, [orderQ.data]);
|
|
|
|
const handleMetaChange = (key: string, value: any) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
meta: {
|
|
...prev.meta,
|
|
[key]: value,
|
|
},
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Existing order form fields */}
|
|
<OrderForm data={formData} onChange={setFormData} />
|
|
|
|
{/* Custom meta fields (Level 1 compatibility) */}
|
|
{metaFields.length > 0 && (
|
|
<MetaFields
|
|
meta={formData.meta}
|
|
fields={metaFields}
|
|
onChange={handleMetaChange}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 3: PHP Registry System (2-3 days)
|
|
|
|
#### 3.1 MetaFieldsRegistry Class
|
|
|
|
**File:** `includes/Compat/MetaFieldsRegistry.php`
|
|
|
|
**Purpose:** Allow plugins to register meta fields for display in SPA
|
|
|
|
```php
|
|
<?php
|
|
namespace WooNooW\Compat;
|
|
|
|
class MetaFieldsRegistry {
|
|
|
|
private static $order_fields = [];
|
|
private static $product_fields = [];
|
|
|
|
public static function init() {
|
|
add_action('admin_enqueue_scripts', [__CLASS__, 'localize_fields']);
|
|
|
|
// Allow plugins to register fields
|
|
do_action('woonoow/register_meta_fields');
|
|
}
|
|
|
|
/**
|
|
* Register order meta field
|
|
*
|
|
* @param string $key Meta key (e.g., '_tracking_number')
|
|
* @param array $args Field configuration
|
|
*/
|
|
public static function register_order_field($key, $args = []) {
|
|
$defaults = [
|
|
'key' => $key,
|
|
'label' => self::format_label($key),
|
|
'type' => 'text',
|
|
'section' => 'Additional Fields',
|
|
'description' => '',
|
|
'placeholder' => '',
|
|
];
|
|
|
|
self::$order_fields[$key] = array_merge($defaults, $args);
|
|
|
|
// Auto-add to allowed meta lists
|
|
add_filter('woonoow/order_allowed_private_meta', function($allowed) use ($key) {
|
|
if (!in_array($key, $allowed, true)) {
|
|
$allowed[] = $key;
|
|
}
|
|
return $allowed;
|
|
});
|
|
|
|
add_filter('woonoow/order_updatable_meta', function($allowed) use ($key) {
|
|
if (!in_array($key, $allowed, true)) {
|
|
$allowed[] = $key;
|
|
}
|
|
return $allowed;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register product meta field
|
|
*/
|
|
public static function register_product_field($key, $args = []) {
|
|
$defaults = [
|
|
'key' => $key,
|
|
'label' => self::format_label($key),
|
|
'type' => 'text',
|
|
'section' => 'Additional Fields',
|
|
'description' => '',
|
|
'placeholder' => '',
|
|
];
|
|
|
|
self::$product_fields[$key] = array_merge($defaults, $args);
|
|
|
|
// Auto-add to allowed meta lists
|
|
add_filter('woonoow/product_allowed_private_meta', function($allowed) use ($key) {
|
|
if (!in_array($key, $allowed, true)) {
|
|
$allowed[] = $key;
|
|
}
|
|
return $allowed;
|
|
});
|
|
|
|
add_filter('woonoow/product_updatable_meta', function($allowed) use ($key) {
|
|
if (!in_array($key, $allowed, true)) {
|
|
$allowed[] = $key;
|
|
}
|
|
return $allowed;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Format meta key to human-readable label
|
|
*/
|
|
private static function format_label($key) {
|
|
// Remove leading underscore
|
|
$label = ltrim($key, '_');
|
|
|
|
// Replace underscores with spaces
|
|
$label = str_replace('_', ' ', $label);
|
|
|
|
// Capitalize words
|
|
$label = ucwords($label);
|
|
|
|
return $label;
|
|
}
|
|
|
|
/**
|
|
* Localize fields to JavaScript
|
|
*/
|
|
public static function localize_fields() {
|
|
if (!is_admin()) return;
|
|
|
|
// Allow plugins to modify fields before localizing
|
|
$order_fields = apply_filters('woonoow/meta_fields_orders', array_values(self::$order_fields));
|
|
$product_fields = apply_filters('woonoow/meta_fields_products', array_values(self::$product_fields));
|
|
|
|
wp_localize_script('woonoow-admin', 'WooNooWMetaFields', [
|
|
'orders' => $order_fields,
|
|
'products' => $product_fields,
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3.2 Initialize Registry
|
|
|
|
**File:** `includes/Core/Plugin.php`
|
|
|
|
```php
|
|
// Add to init() method
|
|
\WooNooW\Compat\MetaFieldsRegistry::init();
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Plan
|
|
|
|
### Test Case 1: WooCommerce Shipment Tracking
|
|
```php
|
|
// Plugin stores tracking number
|
|
update_post_meta($order_id, '_tracking_number', '1234567890');
|
|
|
|
// Expected: Field visible in WooNooW order edit
|
|
// Expected: Can edit and save tracking number
|
|
```
|
|
|
|
### Test Case 2: Advanced Custom Fields (ACF)
|
|
```php
|
|
// ACF stores custom field
|
|
update_post_meta($product_id, 'custom_field', 'value');
|
|
|
|
// Expected: Field visible in WooNooW product edit
|
|
// Expected: Can edit and save custom field
|
|
```
|
|
|
|
### Test Case 3: Custom Metabox Plugin
|
|
```php
|
|
// Plugin registers field
|
|
add_action('woonoow/register_meta_fields', function() {
|
|
\WooNooW\Compat\MetaFieldsRegistry::register_order_field('_custom_field', [
|
|
'label' => 'Custom Field',
|
|
'type' => 'text',
|
|
'section' => 'My Plugin',
|
|
]);
|
|
});
|
|
|
|
// Expected: Field appears in "My Plugin" section
|
|
// Expected: Can edit and save
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### Backend (PHP)
|
|
- [ ] Add `get_order_meta_data()` to OrdersController
|
|
- [ ] Add `update_order_meta_data()` to OrdersController
|
|
- [ ] Add `get_product_meta_data()` to ProductsController
|
|
- [ ] Add `update_product_meta_data()` to ProductsController
|
|
- [ ] Add filters: `woonoow/order_allowed_private_meta`
|
|
- [ ] Add filters: `woonoow/order_updatable_meta`
|
|
- [ ] Add filters: `woonoow/product_allowed_private_meta`
|
|
- [ ] Add filters: `woonoow/product_updatable_meta`
|
|
- [ ] Add filters: `woonoow/order_api_data`
|
|
- [ ] Add filters: `woonoow/product_api_data`
|
|
- [ ] Add actions: `woonoow/order_updated`
|
|
- [ ] Add actions: `woonoow/product_updated`
|
|
- [ ] Create `MetaFieldsRegistry.php`
|
|
- [ ] Add action: `woonoow/register_meta_fields`
|
|
- [ ] Initialize registry in Plugin.php
|
|
|
|
### Frontend (React/TypeScript)
|
|
- [ ] Create `MetaFields.tsx` component
|
|
- [ ] Create `useMetaFields.ts` hook
|
|
- [ ] Update `Orders/Edit.tsx` to include meta fields
|
|
- [ ] Update `Orders/View.tsx` to display meta fields (read-only)
|
|
- [ ] Update `Products/Edit.tsx` to include meta fields
|
|
- [ ] Add meta fields to Product detail page
|
|
|
|
### Testing
|
|
- [ ] Test with WooCommerce Shipment Tracking
|
|
- [ ] Test with ACF (Advanced Custom Fields)
|
|
- [ ] Test with custom metabox plugin
|
|
- [ ] Test meta data save/update
|
|
- [ ] Test meta data display in detail view
|
|
- [ ] Test field registration via `woonoow/register_meta_fields`
|
|
|
|
---
|
|
|
|
## Timeline
|
|
|
|
- **Phase 1 (Backend):** 2-3 days
|
|
- **Phase 2 (Frontend):** 3-4 days
|
|
- **Phase 3 (Registry):** 2-3 days
|
|
- **Testing:** 1-2 days
|
|
|
|
**Total:** 8-12 days (1.5-2 weeks)
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
✅ Plugins using standard WP/WooCommerce meta storage work automatically
|
|
✅ No special integration needed from plugin developers
|
|
✅ Meta fields visible and editable in WooNooW admin
|
|
✅ Data saved correctly to WooCommerce database
|
|
✅ Compatible with popular plugins (Shipment Tracking, ACF, etc.)
|
|
✅ Follows 3-level compatibility strategy
|
|
✅ Zero coupling with specific plugins
|
|
✅ Community does NOTHING extra for Level 1 compatibility
|